Skip to content

Commit d1f0d53

Browse files
authored
Resolve impl_loader issue if step_impl is the first part of the module to import
2 parents e70c255 + 8b8d983 commit d1f0d53

File tree

5 files changed

+94
-57
lines changed

5 files changed

+94
-57
lines changed

getgauge/impl_loader.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import traceback
1010
from contextlib import contextmanager
1111
from os import path
12+
from pathlib import Path
13+
from typing import Optional
1214

1315
from getgauge import logger
1416
from getgauge.registry import registry
@@ -19,28 +21,34 @@
1921
env_dir = os.path.join(project_root, 'env', 'default')
2022
requirements_file = os.path.join(project_root, 'requirements.txt')
2123
sys.path.append(project_root)
22-
temporary_sys_path = []
24+
2325
PLUGIN_JSON = 'python.json'
2426
VERSION = 'version'
2527
PYTHON_PROPERTIES = 'python.properties'
2628
SKEL = 'skel'
2729

2830

29-
def load_impls(step_impl_dirs=impl_dirs):
31+
def load_impls(step_impl_dirs=impl_dirs, project_root=project_root):
32+
""" project_root can be overwritten in tests! """
33+
3034
os.chdir(project_root)
35+
3136
for impl_dir in step_impl_dirs:
32-
if not os.path.isdir(impl_dir):
33-
logger.error('Cannot import step implementations. Error: {} does not exist.'.format(step_impl_dirs))
37+
38+
resolved_impl_dir = Path(impl_dir).resolve()
39+
if not resolved_impl_dir.is_dir():
40+
logger.error('Cannot import step implementations. Error: {} does not exist.'.format(impl_dir))
3441
logger.error('Make sure `STEP_IMPL_DIR` env var is set to a valid directory path.')
3542
return
36-
base_dir = project_root if impl_dir.startswith(project_root) else os.path.dirname(impl_dir)
37-
# Handle multi-level relative imports
38-
for _ in range(impl_dir.count('..')):
39-
base_dir = os.path.dirname(base_dir).replace("/", os.path.sep).replace("\\", os.path.sep)
40-
# Add temporary sys path for relative imports that is not already added
41-
if '..' in impl_dir and base_dir not in temporary_sys_path:
42-
temporary_sys_path.append(base_dir)
43-
_import_impl(base_dir, impl_dir)
43+
44+
base_dir = os.path.commonpath([project_root, f"{resolved_impl_dir}"])
45+
logger.debug("Base directory '{}' of '{}'".format(base_dir, resolved_impl_dir))
46+
47+
temporary_sys_path = None
48+
if project_root != base_dir:
49+
temporary_sys_path = base_dir
50+
51+
_import_impl(base_dir, resolved_impl_dir, temporary_sys_path)
4452

4553

4654
def copy_skel_files():
@@ -58,29 +66,33 @@ def copy_skel_files():
5866
logger.fatal('Exception occurred while copying skel files.\n{}.'.format(traceback.format_exc()))
5967

6068

61-
def _import_impl(base_dir, step_impl_dir):
62-
for python_file in glob.glob(f"{step_impl_dir}/**/*.py", recursive=True):
63-
_import_file(base_dir, python_file)
69+
def _import_impl(base_dir: str, absolute_step_impl_dir: str, temporary_sys_path: Optional[str]):
70+
for python_file in glob.glob(f"{absolute_step_impl_dir}/**/*.py", recursive=True):
71+
relative_path = Path(python_file).relative_to(base_dir)
72+
module_name = ".".join(relative_path.parts).replace(".py", "")
73+
_import_file(module_name, python_file, temporary_sys_path)
6474

6575
@contextmanager
66-
def use_temporary_sys_path():
76+
def use_temporary_sys_path(temporary_sys_path: str):
6777
original_sys_path = sys.path[:]
68-
sys.path.extend(temporary_sys_path)
78+
sys.path.append(temporary_sys_path)
6979
try:
7080
yield
7181
finally:
7282
sys.path = original_sys_path
7383

74-
def _import_file(base_dir, file_path):
75-
rel_path = os.path.normpath(file_path.replace(base_dir + os.path.sep, ''))
84+
def _import_file(module_name: str, file_path: str, temporary_sys_path: Optional[str]):
7685
try:
77-
module_name = os.path.splitext(rel_path.replace(os.path.sep, '.'))[0]
86+
logger.debug('Import module {} with path {}'.format(module_name, file_path))
87+
7888
# Use temporary sys path for relative imports
79-
if '..' in file_path:
80-
with use_temporary_sys_path():
89+
if temporary_sys_path is not None:
90+
logger.debug('Import module {} using temporary sys path {}'.format(module_name, temporary_sys_path))
91+
with use_temporary_sys_path(temporary_sys_path):
8192
m = importlib.import_module(module_name)
8293
else:
8394
m = importlib.import_module(module_name)
95+
8496
# Get all classes in the imported module
8597
classes = inspect.getmembers(m, lambda member: inspect.isclass(member) and member.__module__ == module_name)
8698
if len(classes) > 0:
@@ -92,13 +104,10 @@ def _import_file(base_dir, file_path):
92104
file_path=file_path
93105
)
94106
except:
95-
logger.fatal('Exception occurred while loading step implementations from file: {}.\n{}'.format(rel_path, traceback.format_exc()))
107+
logger.fatal('Exception occurred while loading step implementations from file: {}.\n{}'.format(file_path, traceback.format_exc()))
96108

97-
def update_step_registry_with_class(instance, file_path):
109+
def update_step_registry_with_class(instance, file_path: str):
98110
""" Inject instance in each class method (hook/step) """
99-
# Resolve the absolute path from relative path
100-
# Note: relative path syntax ".." can appear in between the file_path too like "<Project_Root>/../../Other_Project/src/step_impl/file.py"
101-
file_path = os.path.abspath(file_path) if ".." in str(file_path) else file_path
102111
method_list = registry.get_all_methods_in(file_path)
103112
for info in method_list:
104113
class_methods = [x[0] for x in inspect.getmembers(instance, inspect.ismethod)]

getgauge/registry.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def __init__(self):
113113
for hook in Registry.hooks:
114114
self.__def_hook(hook)
115115

116+
def get_steps_map(self):
117+
return self.__steps_map
118+
116119
def __def_hook(self, hook):
117120
def get(self, tags=None):
118121
return _filter_hooks(tags, getattr(self, '__{}'.format(hook)))
@@ -185,7 +188,7 @@ def _get_all_hooks(self, file_name):
185188
if paths_equal(h.file_name, file_name)]
186189
return all_hooks
187190

188-
def get_all_methods_in(self, file_name):
191+
def get_all_methods_in(self, file_name: str):
189192
methods = []
190193
for _, infos in self.__steps_map.items():
191194
methods = methods + [i for i in infos if paths_equal(i.file_name, file_name)]
@@ -212,18 +215,8 @@ def clear(self):
212215

213216

214217
def paths_equal(p1: Union[str, Path], p2: Union[str, Path]) -> bool:
215-
"""
216-
Compare two paths in a cross-platform safe way.
217-
On Windows: case-insensitive, slash-insensitive.
218-
On Linux/macOS: case-sensitive.
219-
"""
220-
p1 = Path(p1).resolve()
221-
p2 = Path(p2).resolve()
222-
if sys.platform.startswith("win"):
223-
# As Windows is case-insensitive, we can use 'normcase' to compare paths!
224-
return os.path.normcase(str(p1)) == os.path.normcase(str(p2))
225-
# Mac (and others) allows to use case-sensitive files/folders!
226-
return p1 == p2
218+
""" Normalize paths in order to compare them. """
219+
return os.path.normcase(str(p1)) == os.path.normcase(str(p2))
227220

228221

229222
def _filter_hooks(tags, hooks):

tests/test_impl_loader.py

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,65 @@
11
import os
22
import unittest
3+
from pathlib import Path
34

4-
from getgauge.impl_loader import update_step_registry_with_class
5-
from test_relative_import.relative_import_class import Sample
5+
from getgauge.impl_loader import load_impls
6+
from getgauge.registry import registry
7+
8+
DIRECTORY_NAME = "test_relative_import"
69

710

811
class ImplLoaderTest(unittest.TestCase):
9-
def setUp(self):
10-
self.curr_dir = os.getcwd()
11-
self.relative_file_path = os.path.join('..', 'test_relative_import', 'relative_import_class.py')
12-
self.relative_file_path_one_level_above = os.path.join('tests', '..', 'test_relative_import', 'relative_import_class.py')
1312

1413
def test_update_step_registry_with_class(self):
15-
os.chdir('tests')
16-
method_list = update_step_registry_with_class(Sample(), self.relative_file_path)
17-
os.chdir(self.curr_dir)
18-
self.assertEqual(["Greet <name> from inside the class",
19-
"Greet <name> from outside the class"],
20-
[method.step_text for method in method_list])
14+
15+
test_relative_import_directory = str(Path(__file__).resolve().parent / DIRECTORY_NAME)
16+
relative_file_path = os.path.join('..', DIRECTORY_NAME)
17+
18+
load_impls(
19+
step_impl_dirs=[relative_file_path],
20+
project_root=test_relative_import_directory
21+
)
22+
23+
loaded_steps = registry.get_steps_map()
24+
25+
self.assertEqual(2, len(loaded_steps))
26+
27+
step_infos_of_class_instance = loaded_steps["Greet {} from inside the class"]
28+
29+
self.assertEqual(1, len(step_infos_of_class_instance))
30+
self.assertIsNotNone(step_infos_of_class_instance[0].instance)
31+
32+
self.assertEqual(
33+
["Greet <name> from inside the class", "Greet <name> from outside the class"],
34+
registry.steps()
35+
)
2136

2237
def test_update_step_registry_with_class_one_level_above(self):
23-
os.chdir(self.curr_dir)
24-
method_list = update_step_registry_with_class(Sample(), self.relative_file_path_one_level_above)
25-
self.assertEqual(["Greet <name> from inside the class",
26-
"Greet <name> from outside the class"],
27-
[method.step_text for method in method_list])
38+
39+
repo_root_directory = str(Path(__file__).resolve().parent.parent)
40+
relative_file_path_one_level_above = os.path.join('tests', '..', 'tests', DIRECTORY_NAME)
41+
42+
load_impls(
43+
step_impl_dirs=[relative_file_path_one_level_above],
44+
project_root=repo_root_directory
45+
)
46+
47+
loaded_steps = registry.get_steps_map()
48+
49+
self.assertEqual(2, len(loaded_steps), f"Steps found: {loaded_steps}")
50+
51+
step_infos_of_class_instance = loaded_steps["Greet {} from inside the class"]
52+
53+
self.assertEqual(1, len(step_infos_of_class_instance))
54+
self.assertIsNotNone(step_infos_of_class_instance[0].instance)
55+
56+
self.assertEqual(
57+
["Greet <name> from inside the class", "Greet <name> from outside the class"],
58+
registry.steps()
59+
)
60+
61+
def tearDown(self):
62+
registry.clear()
2863

2964

3065
if __name__ == '__main__':
File renamed without changes.

0 commit comments

Comments
 (0)