kdtest-pw 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. kdtest_pw/__init__.py +50 -0
  2. kdtest_pw/action/__init__.py +7 -0
  3. kdtest_pw/action/base_keyword.py +292 -0
  4. kdtest_pw/action/element_plus/__init__.py +23 -0
  5. kdtest_pw/action/element_plus/el_cascader.py +263 -0
  6. kdtest_pw/action/element_plus/el_datepicker.py +324 -0
  7. kdtest_pw/action/element_plus/el_dialog.py +317 -0
  8. kdtest_pw/action/element_plus/el_form.py +443 -0
  9. kdtest_pw/action/element_plus/el_menu.py +456 -0
  10. kdtest_pw/action/element_plus/el_select.py +268 -0
  11. kdtest_pw/action/element_plus/el_table.py +442 -0
  12. kdtest_pw/action/element_plus/el_tree.py +364 -0
  13. kdtest_pw/action/element_plus/el_upload.py +313 -0
  14. kdtest_pw/action/key_retrieval.py +311 -0
  15. kdtest_pw/action/page_action.py +1129 -0
  16. kdtest_pw/api/__init__.py +6 -0
  17. kdtest_pw/api/api_keyword.py +251 -0
  18. kdtest_pw/api/request_handler.py +232 -0
  19. kdtest_pw/cases/__init__.py +6 -0
  20. kdtest_pw/cases/case_collector.py +182 -0
  21. kdtest_pw/cases/case_executor.py +359 -0
  22. kdtest_pw/cases/read/__init__.py +6 -0
  23. kdtest_pw/cases/read/cell_handler.py +305 -0
  24. kdtest_pw/cases/read/excel_reader.py +223 -0
  25. kdtest_pw/cli/__init__.py +5 -0
  26. kdtest_pw/cli/run.py +318 -0
  27. kdtest_pw/common.py +106 -0
  28. kdtest_pw/core/__init__.py +7 -0
  29. kdtest_pw/core/browser_manager.py +196 -0
  30. kdtest_pw/core/config_loader.py +235 -0
  31. kdtest_pw/core/page_context.py +228 -0
  32. kdtest_pw/data/__init__.py +5 -0
  33. kdtest_pw/data/init_data.py +105 -0
  34. kdtest_pw/data/static/elementData.yaml +59 -0
  35. kdtest_pw/data/static/parameters.json +24 -0
  36. kdtest_pw/plugins/__init__.py +6 -0
  37. kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
  38. kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
  39. kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
  40. kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
  41. kdtest_pw/plugins/plugin_base.py +180 -0
  42. kdtest_pw/plugins/plugin_loader.py +260 -0
  43. kdtest_pw/product.py +5 -0
  44. kdtest_pw/reference.py +99 -0
  45. kdtest_pw/utils/__init__.py +13 -0
  46. kdtest_pw/utils/built_in_function.py +376 -0
  47. kdtest_pw/utils/decorator.py +211 -0
  48. kdtest_pw/utils/log/__init__.py +6 -0
  49. kdtest_pw/utils/log/html_report.py +336 -0
  50. kdtest_pw/utils/log/logger.py +123 -0
  51. kdtest_pw/utils/public_script.py +366 -0
  52. kdtest_pw-2.0.0.dist-info/METADATA +169 -0
  53. kdtest_pw-2.0.0.dist-info/RECORD +57 -0
  54. kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
  55. kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
  56. kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
  57. kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,6 @@
1
+ """API 测试模块"""
2
+
3
+ from .request_handler import RequestHandler
4
+ from .api_keyword import ApiKeyword
5
+
6
+ __all__ = ['RequestHandler', 'ApiKeyword']
@@ -0,0 +1,251 @@
1
+ """API 测试关键字"""
2
+
3
+ import json
4
+ from typing import Any, Dict, Optional
5
+
6
+ from .request_handler import RequestHandler
7
+ from ..reference import INFO, set_element_value, get_element_value
8
+
9
+
10
+ class ApiKeyword:
11
+ """API 测试关键字
12
+
13
+ 提供 API 测试的关键字方法。
14
+ """
15
+
16
+ def __init__(self, request_handler: RequestHandler = None):
17
+ """初始化 API 关键字
18
+
19
+ Args:
20
+ request_handler: 请求处理器
21
+ """
22
+ self._handler = request_handler or RequestHandler()
23
+
24
+ def api_set_base_url(self, *, content: str) -> None:
25
+ """设置基础 URL
26
+
27
+ Args:
28
+ content: 基础 URL
29
+ """
30
+ self._handler._base_url = content
31
+ INFO(f"设置基础 URL: {content}")
32
+
33
+ def api_set_header(self, *, content: str) -> None:
34
+ """设置请求头
35
+
36
+ Args:
37
+ content: JSON 格式的请求头 {"key": "value"}
38
+ """
39
+ headers = json.loads(content)
40
+ self._handler.set_default_headers(headers)
41
+ INFO(f"设置请求头: {list(headers.keys())}")
42
+
43
+ def api_get(self, *, content: str, key: str = None) -> Dict[str, Any]:
44
+ """GET 请求
45
+
46
+ Args:
47
+ content: URL 或 JSON 配置 {"url": "...", "params": {...}}
48
+ key: 响应缓存键名
49
+
50
+ Returns:
51
+ Dict: 响应数据
52
+ """
53
+ if content.startswith('{'):
54
+ config = json.loads(content)
55
+ url = config.get('url', '')
56
+ params = config.get('params')
57
+ headers = config.get('headers')
58
+ response = self._handler.get(url, params=params, headers=headers)
59
+ else:
60
+ response = self._handler.get(content)
61
+
62
+ if key:
63
+ set_element_value(key, response)
64
+
65
+ return response
66
+
67
+ def api_post(self, *, content: str, key: str = None) -> Dict[str, Any]:
68
+ """POST 请求
69
+
70
+ Args:
71
+ content: JSON 配置 {"url": "...", "data": {...}}
72
+ key: 响应缓存键名
73
+
74
+ Returns:
75
+ Dict: 响应数据
76
+ """
77
+ config = json.loads(content)
78
+ url = config.get('url', '')
79
+ data = config.get('data')
80
+ json_data = config.get('json')
81
+ headers = config.get('headers')
82
+
83
+ response = self._handler.post(url, data=data, json_data=json_data, headers=headers)
84
+
85
+ if key:
86
+ set_element_value(key, response)
87
+
88
+ return response
89
+
90
+ def api_put(self, *, content: str, key: str = None) -> Dict[str, Any]:
91
+ """PUT 请求
92
+
93
+ Args:
94
+ content: JSON 配置
95
+ key: 响应缓存键名
96
+
97
+ Returns:
98
+ Dict: 响应数据
99
+ """
100
+ config = json.loads(content)
101
+ url = config.get('url', '')
102
+ data = config.get('data')
103
+ json_data = config.get('json')
104
+ headers = config.get('headers')
105
+
106
+ response = self._handler.put(url, data=data, json_data=json_data, headers=headers)
107
+
108
+ if key:
109
+ set_element_value(key, response)
110
+
111
+ return response
112
+
113
+ def api_delete(self, *, content: str, key: str = None) -> Dict[str, Any]:
114
+ """DELETE 请求
115
+
116
+ Args:
117
+ content: URL 或 JSON 配置
118
+ key: 响应缓存键名
119
+
120
+ Returns:
121
+ Dict: 响应数据
122
+ """
123
+ if content.startswith('{'):
124
+ config = json.loads(content)
125
+ url = config.get('url', '')
126
+ headers = config.get('headers')
127
+ response = self._handler.delete(url, headers=headers)
128
+ else:
129
+ response = self._handler.delete(content)
130
+
131
+ if key:
132
+ set_element_value(key, response)
133
+
134
+ return response
135
+
136
+ def api_status_assert(self, *, content: str) -> bool:
137
+ """断言响应状态码
138
+
139
+ Args:
140
+ content: 期望的状态码
141
+
142
+ Returns:
143
+ bool: 断言结果
144
+ """
145
+ response = self._handler.last_response
146
+ if not response:
147
+ INFO("没有响应数据", "ERROR")
148
+ return False
149
+
150
+ expected = int(content)
151
+ actual = response.get('status')
152
+ result = actual == expected
153
+
154
+ INFO(f"状态码断言: {actual} == {expected} -> {result}")
155
+ return result
156
+
157
+ def api_json_assert(self, *, content: str, path: str, mode: str = 'equals') -> bool:
158
+ """断言响应 JSON 字段
159
+
160
+ Args:
161
+ content: 期望值
162
+ path: JSON 路径 (如 "data.user.name")
163
+ mode: 断言模式
164
+
165
+ Returns:
166
+ bool: 断言结果
167
+ """
168
+ response = self._handler.last_response
169
+ if not response or 'json' not in response:
170
+ INFO("没有 JSON 响应数据", "ERROR")
171
+ return False
172
+
173
+ # 获取实际值
174
+ actual = self._get_json_value(response['json'], path)
175
+
176
+ # 断言
177
+ if mode == 'equals':
178
+ result = str(actual) == content
179
+ elif mode == 'contains':
180
+ result = content in str(actual)
181
+ elif mode == 'exists':
182
+ result = actual is not None
183
+ elif mode == 'not_exists':
184
+ result = actual is None
185
+ else:
186
+ result = str(actual) == content
187
+
188
+ INFO(f"JSON 断言: {path}='{actual}' {mode} '{content}' -> {result}")
189
+ return result
190
+
191
+ def api_extract_value(self, *, content: str, path: str) -> Any:
192
+ """提取响应值并缓存
193
+
194
+ Args:
195
+ content: 缓存键名
196
+ path: JSON 路径
197
+
198
+ Returns:
199
+ 提取的值
200
+ """
201
+ response = self._handler.last_response
202
+ if not response or 'json' not in response:
203
+ INFO("没有 JSON 响应数据", "ERROR")
204
+ return None
205
+
206
+ value = self._get_json_value(response['json'], path)
207
+ set_element_value(content, value)
208
+
209
+ INFO(f"提取值: {content} = {path} -> {value}")
210
+ return value
211
+
212
+ def api_get_response(self, *, content: str) -> Optional[Dict[str, Any]]:
213
+ """获取缓存的响应
214
+
215
+ Args:
216
+ content: 缓存键名
217
+
218
+ Returns:
219
+ 响应数据
220
+ """
221
+ return get_element_value(content)
222
+
223
+ def _get_json_value(self, data: Any, path: str) -> Any:
224
+ """从 JSON 获取值
225
+
226
+ Args:
227
+ data: JSON 数据
228
+ path: 路径 (支持点号和数组索引)
229
+
230
+ Returns:
231
+
232
+ """
233
+ keys = path.replace('[', '.').replace(']', '').split('.')
234
+ value = data
235
+
236
+ for key in keys:
237
+ if not key:
238
+ continue
239
+
240
+ if isinstance(value, dict):
241
+ value = value.get(key)
242
+ elif isinstance(value, list) and key.isdigit():
243
+ index = int(key)
244
+ value = value[index] if 0 <= index < len(value) else None
245
+ else:
246
+ return None
247
+
248
+ if value is None:
249
+ return None
250
+
251
+ return value
@@ -0,0 +1,232 @@
1
+ """API 请求处理器"""
2
+
3
+ from typing import Dict, Any, Optional
4
+ from playwright.sync_api import APIRequestContext, BrowserContext
5
+
6
+ from ..reference import GSTORE, INFO, set_element_value
7
+
8
+
9
+ class RequestHandler:
10
+ """API 请求处理器
11
+
12
+ 使用 Playwright 的 APIRequestContext 发送 HTTP 请求。
13
+ """
14
+
15
+ def __init__(self, context: Optional[BrowserContext] = None, base_url: str = ''):
16
+ """初始化请求处理器
17
+
18
+ Args:
19
+ context: BrowserContext 实例
20
+ base_url: 基础 URL
21
+ """
22
+ self._context = context or GSTORE.get('context')
23
+ self._base_url = base_url
24
+ self._request: Optional[APIRequestContext] = None
25
+ self._default_headers: Dict[str, str] = {}
26
+ self._last_response: Optional[Dict[str, Any]] = None
27
+
28
+ def create_request_context(
29
+ self,
30
+ base_url: str = None,
31
+ extra_headers: Dict[str, str] = None
32
+ ) -> None:
33
+ """创建请求上下文
34
+
35
+ Args:
36
+ base_url: 基础 URL
37
+ extra_headers: 额外请求头
38
+ """
39
+ if self._context:
40
+ options = {}
41
+ if base_url:
42
+ options['base_url'] = base_url
43
+ if extra_headers:
44
+ options['extra_http_headers'] = extra_headers
45
+
46
+ self._request = self._context.request
47
+
48
+ def set_default_headers(self, headers: Dict[str, str]) -> None:
49
+ """设置默认请求头
50
+
51
+ Args:
52
+ headers: 请求头字典
53
+ """
54
+ self._default_headers.update(headers)
55
+
56
+ def get(
57
+ self,
58
+ url: str,
59
+ params: Dict[str, Any] = None,
60
+ headers: Dict[str, str] = None
61
+ ) -> Dict[str, Any]:
62
+ """GET 请求
63
+
64
+ Args:
65
+ url: 请求 URL
66
+ params: 查询参数
67
+ headers: 请求头
68
+
69
+ Returns:
70
+ Dict: 响应数据
71
+ """
72
+ return self._request_method('get', url, params=params, headers=headers)
73
+
74
+ def post(
75
+ self,
76
+ url: str,
77
+ data: Any = None,
78
+ json_data: Dict = None,
79
+ headers: Dict[str, str] = None
80
+ ) -> Dict[str, Any]:
81
+ """POST 请求
82
+
83
+ Args:
84
+ url: 请求 URL
85
+ data: 表单数据
86
+ json_data: JSON 数据
87
+ headers: 请求头
88
+
89
+ Returns:
90
+ Dict: 响应数据
91
+ """
92
+ return self._request_method('post', url, data=data, json_data=json_data, headers=headers)
93
+
94
+ def put(
95
+ self,
96
+ url: str,
97
+ data: Any = None,
98
+ json_data: Dict = None,
99
+ headers: Dict[str, str] = None
100
+ ) -> Dict[str, Any]:
101
+ """PUT 请求
102
+
103
+ Args:
104
+ url: 请求 URL
105
+ data: 表单数据
106
+ json_data: JSON 数据
107
+ headers: 请求头
108
+
109
+ Returns:
110
+ Dict: 响应数据
111
+ """
112
+ return self._request_method('put', url, data=data, json_data=json_data, headers=headers)
113
+
114
+ def delete(
115
+ self,
116
+ url: str,
117
+ headers: Dict[str, str] = None
118
+ ) -> Dict[str, Any]:
119
+ """DELETE 请求
120
+
121
+ Args:
122
+ url: 请求 URL
123
+ headers: 请求头
124
+
125
+ Returns:
126
+ Dict: 响应数据
127
+ """
128
+ return self._request_method('delete', url, headers=headers)
129
+
130
+ def patch(
131
+ self,
132
+ url: str,
133
+ data: Any = None,
134
+ json_data: Dict = None,
135
+ headers: Dict[str, str] = None
136
+ ) -> Dict[str, Any]:
137
+ """PATCH 请求
138
+
139
+ Args:
140
+ url: 请求 URL
141
+ data: 表单数据
142
+ json_data: JSON 数据
143
+ headers: 请求头
144
+
145
+ Returns:
146
+ Dict: 响应数据
147
+ """
148
+ return self._request_method('patch', url, data=data, json_data=json_data, headers=headers)
149
+
150
+ def _request_method(
151
+ self,
152
+ method: str,
153
+ url: str,
154
+ params: Dict[str, Any] = None,
155
+ data: Any = None,
156
+ json_data: Dict = None,
157
+ headers: Dict[str, str] = None
158
+ ) -> Dict[str, Any]:
159
+ """发送请求
160
+
161
+ Args:
162
+ method: 请求方法
163
+ url: 请求 URL
164
+ params: 查询参数
165
+ data: 表单数据
166
+ json_data: JSON 数据
167
+ headers: 请求头
168
+
169
+ Returns:
170
+ Dict: 响应数据
171
+ """
172
+ if not self._request and self._context:
173
+ self._request = self._context.request
174
+
175
+ if not self._request:
176
+ raise RuntimeError("请求上下文未初始化")
177
+
178
+ # 合并请求头
179
+ final_headers = {**self._default_headers}
180
+ if headers:
181
+ final_headers.update(headers)
182
+
183
+ # 构建完整 URL
184
+ full_url = f"{self._base_url}{url}" if self._base_url and not url.startswith('http') else url
185
+
186
+ # 构建请求参数
187
+ request_kwargs = {
188
+ 'headers': final_headers if final_headers else None,
189
+ }
190
+
191
+ if params:
192
+ request_kwargs['params'] = params
193
+ if data:
194
+ request_kwargs['data'] = data
195
+ if json_data:
196
+ request_kwargs['data'] = json_data
197
+
198
+ INFO(f"API {method.upper()}: {full_url}")
199
+
200
+ # 发送请求
201
+ request_method = getattr(self._request, method)
202
+ response = request_method(full_url, **{k: v for k, v in request_kwargs.items() if v is not None})
203
+
204
+ # 解析响应
205
+ result = {
206
+ 'status': response.status,
207
+ 'status_text': response.status_text,
208
+ 'headers': dict(response.headers),
209
+ 'url': response.url,
210
+ }
211
+
212
+ # 尝试解析 JSON
213
+ try:
214
+ result['json'] = response.json()
215
+ except Exception:
216
+ result['text'] = response.text()
217
+
218
+ self._last_response = result
219
+ INFO(f"API 响应: {response.status} {response.status_text}")
220
+
221
+ return result
222
+
223
+ @property
224
+ def last_response(self) -> Optional[Dict[str, Any]]:
225
+ """获取最后一次响应"""
226
+ return self._last_response
227
+
228
+ def dispose(self) -> None:
229
+ """释放资源"""
230
+ if self._request:
231
+ self._request.dispose()
232
+ self._request = None
@@ -0,0 +1,6 @@
1
+ """测试用例模块"""
2
+
3
+ from .case_collector import CaseCollector
4
+ from .case_executor import CaseExecutor
5
+
6
+ __all__ = ['CaseCollector', 'CaseExecutor']
@@ -0,0 +1,182 @@
1
+ """测试用例收集器"""
2
+
3
+ from typing import List, Dict, Any, Optional
4
+ from pathlib import Path
5
+
6
+ from .read.excel_reader import ExcelReader, TestCase, TestStep
7
+ from ..reference import INFO
8
+
9
+
10
+ class CaseCollector:
11
+ """测试用例收集器
12
+
13
+ 从配置和 Excel 文件收集测试用例。
14
+ """
15
+
16
+ def __init__(self, config: Dict[str, Any] = None):
17
+ """初始化用例收集器
18
+
19
+ Args:
20
+ config: 配置字典
21
+ """
22
+ self._config = config or {}
23
+ self._cases: List[TestCase] = []
24
+ self._case_files: List[Dict[str, Any]] = []
25
+
26
+ def add_case_file(
27
+ self,
28
+ file_path: str,
29
+ sheets: List[str] = None,
30
+ interface_switch: bool = False
31
+ ) -> None:
32
+ """添加测试用例文件
33
+
34
+ Args:
35
+ file_path: Excel 文件路径
36
+ sheets: 要执行的 Sheet 列表,None 表示全部
37
+ interface_switch: 是否接口测试
38
+ """
39
+ self._case_files.append({
40
+ 'path': file_path,
41
+ 'sheets': sheets,
42
+ 'interface': interface_switch
43
+ })
44
+
45
+ def collect(self) -> List[TestCase]:
46
+ """收集所有测试用例
47
+
48
+ Returns:
49
+ List[TestCase]: 测试用例列表
50
+ """
51
+ self._cases.clear()
52
+
53
+ # 从配置收集
54
+ if 'testCaseFile' in self._config:
55
+ for file_config in self._config['testCaseFile']:
56
+ self.add_case_file(
57
+ file_path=file_config.get('caseFilePath', ''),
58
+ sheets=file_config.get('caseItem'),
59
+ interface_switch=file_config.get('interfaceSwitch', False)
60
+ )
61
+
62
+ # 收集用例
63
+ for file_info in self._case_files:
64
+ self._collect_from_file(file_info)
65
+
66
+ INFO(f"共收集 {len(self._cases)} 个测试用例")
67
+ return self._cases
68
+
69
+ def _collect_from_file(self, file_info: Dict[str, Any]) -> None:
70
+ """从文件收集用例
71
+
72
+ Args:
73
+ file_info: 文件信息
74
+ """
75
+ file_path = file_info['path']
76
+ sheets = file_info['sheets']
77
+
78
+ if not Path(file_path).exists():
79
+ INFO(f"用例文件不存在: {file_path}", "WARNING")
80
+ return
81
+
82
+ try:
83
+ with ExcelReader(file_path) as reader:
84
+ if sheets:
85
+ # 只读取指定 Sheet
86
+ all_cases = reader.read_all_sheets(sheets)
87
+ else:
88
+ # 读取所有 Sheet
89
+ all_cases = reader.read_all_sheets()
90
+
91
+ for sheet_name, cases in all_cases.items():
92
+ for case_data in cases:
93
+ case_data['file'] = file_path
94
+ case_data['interface'] = file_info['interface']
95
+ self._cases.append(TestCase(case_data))
96
+
97
+ except Exception as e:
98
+ INFO(f"读取用例文件失败: {file_path} - {e}", "ERROR")
99
+
100
+ @property
101
+ def cases(self) -> List[TestCase]:
102
+ """获取所有测试用例"""
103
+ return self._cases.copy()
104
+
105
+ @property
106
+ def case_count(self) -> int:
107
+ """获取用例数量"""
108
+ return len(self._cases)
109
+
110
+ def get_cases_by_sheet(self, sheet_name: str) -> List[TestCase]:
111
+ """按 Sheet 获取用例
112
+
113
+ Args:
114
+ sheet_name: Sheet 名称
115
+
116
+ Returns:
117
+ List[TestCase]: 匹配的用例列表
118
+ """
119
+ return [c for c in self._cases if c.sheet == sheet_name]
120
+
121
+ def get_cases_by_name(self, name_pattern: str) -> List[TestCase]:
122
+ """按名称模式获取用例
123
+
124
+ Args:
125
+ name_pattern: 名称模式(支持通配符 *)
126
+
127
+ Returns:
128
+ List[TestCase]: 匹配的用例列表
129
+ """
130
+ import fnmatch
131
+ return [c for c in self._cases if fnmatch.fnmatch(c.name, name_pattern)]
132
+
133
+ def get_cases_by_id(self, case_ids: List[str]) -> List[TestCase]:
134
+ """按 ID 获取用例
135
+
136
+ Args:
137
+ case_ids: 用例 ID 列表
138
+
139
+ Returns:
140
+ List[TestCase]: 匹配的用例列表
141
+ """
142
+ return [c for c in self._cases if c.id in case_ids]
143
+
144
+ def filter_cases(
145
+ self,
146
+ include_sheets: List[str] = None,
147
+ exclude_sheets: List[str] = None,
148
+ include_ids: List[str] = None,
149
+ exclude_ids: List[str] = None,
150
+ name_pattern: str = None
151
+ ) -> List[TestCase]:
152
+ """过滤用例
153
+
154
+ Args:
155
+ include_sheets: 包含的 Sheet
156
+ exclude_sheets: 排除的 Sheet
157
+ include_ids: 包含的 ID
158
+ exclude_ids: 排除的 ID
159
+ name_pattern: 名称模式
160
+
161
+ Returns:
162
+ List[TestCase]: 过滤后的用例列表
163
+ """
164
+ result = self._cases.copy()
165
+
166
+ if include_sheets:
167
+ result = [c for c in result if c.sheet in include_sheets]
168
+
169
+ if exclude_sheets:
170
+ result = [c for c in result if c.sheet not in exclude_sheets]
171
+
172
+ if include_ids:
173
+ result = [c for c in result if c.id in include_ids]
174
+
175
+ if exclude_ids:
176
+ result = [c for c in result if c.id not in exclude_ids]
177
+
178
+ if name_pattern:
179
+ import fnmatch
180
+ result = [c for c in result if fnmatch.fnmatch(c.name, name_pattern)]
181
+
182
+ return result