-
Notifications
You must be signed in to change notification settings - Fork 4
Robustness changes #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,9 +99,12 @@ def get_package_name(obj: object | None = None) -> str: | |
|
|
||
| frame = inspect.currentframe() | ||
| parent = frame.f_back if frame is not None else None | ||
| if parent is None: | ||
| return "" | ||
| return _installed_package_from_globals(parent.f_globals) | ||
| try: | ||
| if parent is None: | ||
| return "" | ||
| return _installed_package_from_globals(parent.f_globals) | ||
| finally: | ||
| del frame, parent | ||
|
|
||
|
|
||
| def _version_via_metadata(name: str) -> str | None: | ||
|
|
@@ -516,6 +519,7 @@ def log_args(self, args: dict[str, object] | None = None) -> None: | |
| frame = inspect.currentframe() | ||
| parent = frame.f_back if frame is not None else None | ||
| args = inspect.getargvalues(parent).locals if parent is not None else {} | ||
| del frame, parent | ||
|
|
||
| result = { | ||
| k: args[k] | ||
|
|
@@ -545,8 +549,10 @@ def _log_metadata( | |
| Unions ``caller_name``'s installed dependencies with ``packages``, | ||
| resolves each name via ``value_for``, then emits ``name==value`` | ||
| lines under ``label`` with the caller first, the rest | ||
| alphabetical. Lookups happen eagerly so a failed resolution aborts | ||
| before any line is written. | ||
| alphabetical. A ``None`` value is normalised to the ``"UNKNOWN"`` | ||
| sentinel so a line is never written as ``name==None``. Lookups | ||
| happen eagerly so a failed resolution aborts before any line is | ||
| written. | ||
| """ | ||
| caller_value: str | None = None | ||
| if caller_name: | ||
|
Comment on lines
+552
to
558
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Align docstring and behavior for The new docstring matches the Suggested implementation: caller_value: str | None = None
if caller_name is not None:
caller_value = value_for(caller_name)
entries.append((caller_name, caller_value))To fully align behavior with the docstring (all
|
||
|
|
@@ -582,7 +588,8 @@ def _log_metadata( | |
| entries.append((pkg, value_for(pkg))) | ||
|
|
||
| for name, value in entries: | ||
| self.log_message(f"{name}=={value}", label=label) | ||
| resolved = value if value is not None else "UNKNOWN" | ||
| self.log_message(f"{name}=={resolved}", label=label) | ||
|
|
||
| def log_versions(self, packages: list[str] | str | None = None) -> None: | ||
| """logs the caller's package, its installed dependencies, and named packages | ||
|
|
@@ -611,6 +618,7 @@ def log_versions(self, packages: list[str] | str | None = None) -> None: | |
| if parent is not None | ||
| else "" | ||
| ) | ||
| del frame, parent | ||
| self._log_metadata( | ||
| packages, | ||
| get_version_for_package, | ||
|
|
@@ -644,6 +652,7 @@ def log_licenses(self, packages: list[str] | str | None = None) -> None: | |
| if parent is not None | ||
| else "" | ||
| ) | ||
| del frame, parent | ||
| self._log_metadata( | ||
| packages, | ||
| _license_for_package, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -369,6 +369,23 @@ def test_tracks_versions_module(logfile): | |
| assert expect in line, line | ||
|
|
||
|
|
||
| def test_log_versions_unresolvable_version_logs_unknown(logfile): | ||
| # a module with no version attribute -> version None normalised to UNKNOWN | ||
| pyfile = TEST_ROOTDIR / "delme_nover.py" | ||
| pyfile.write_text("answer = 42\n") | ||
| sys.path.append(str(TEST_ROOTDIR)) | ||
| import delme_nover | ||
|
|
||
| LOGGER = CachingLogger(create_dir=True) | ||
| LOGGER.log_file_path = logfile | ||
| LOGGER.log_versions(delme_nover) | ||
|
Comment on lines
+372
to
+381
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): Clean up sys.path and sys.modules mutations in the new test to avoid cross-test interference. This test mutates global state by appending to sys.path and importing delme_nover without cleanup, which can cause ordering-dependent failures in other tests. Please wrap the sys.path change in a try/finally and remove the path entry, and delete sys.modules['delme_nover'] after the test (guarding for KeyError) so it doesn’t leak global state. Suggested implementation: def test_log_versions_unresolvable_version_logs_unknown(logfile):
# a module with no version attribute -> version None normalised to UNKNOWN
pyfile = TEST_ROOTDIR / "delme_nover.py"
pyfile.write_text("answer = 42\n")
module_name = "delme_nover"
path_entry = str(TEST_ROOTDIR)
sys.path.append(path_entry)
try:
delme_nover = __import__(module_name)
LOGGER = CachingLogger(create_dir=True)
LOGGER.log_file_path = logfile
LOGGER.log_versions(delme_nover)
LOGGER.shutdown()
lines = logfile.read_text().splitlines()
assert any("delme_nover==UNKNOWN" in line for line in lines)
assert not any("delme_nover==None" in line for line in lines)
finally:
# Clean up global state and temporary file to avoid cross-test interference
if path_entry in sys.path:
sys.path.remove(path_entry)
sys.modules.pop(module_name, None)
if pyfile.exists():
pyfile.unlink()
|
||
| LOGGER.shutdown() | ||
| pyfile.unlink() | ||
| lines = logfile.read_text().splitlines() | ||
| assert any("delme_nover==UNKNOWN" in line for line in lines) | ||
| assert not any("delme_nover==None" in line for line in lines) | ||
|
|
||
|
|
||
| def test_get_package_dependencies_not_installed(monkeypatch): | ||
| # unknown package -> empty dict (never raises) | ||
| def fake_requires(name): | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): Use
try/finallyaroundinspect.currentframe()usage to guarantee cleanup on exceptions.In
log_args,log_versions, andlog_licenses,del frame, parentonly runs on the happy path; any exception before it will keep the frame alive and maintain the reference cycle. To matchget_package_nameand avoid leaks in long‑lived processes, please wrap eachinspect.currentframe()usage in atry/finallyso cleanup always occurs.Suggested implementation:
From your comment,
get_package_namealready uses a correcttry/finallypattern. To fully implement your suggestion:inspect.currentframe()in this file, specifically inlog_args,log_versions, andlog_licenses.frame/parentin atryblock and placedel frame, parentin a correspondingfinallyblock, mirroring the pattern used inget_package_nameand the edits above.finallyblock so that frame cleanup is guaranteed.