drop support for Python 3.9#10634
Merged
radoering merged 2 commits intopython-poetry:mainfrom Dec 7, 2025
Merged
Conversation
Reviewer's GuideDrops Python 3.9 support and standardizes the codebase, tooling, and tests on Python 3.10+, including migrating to stdlib importlib.metadata, simplifying encoding/newline handling, and modernizing type hints and CI/test configuration. Sequence diagram for deprecated metadata access via poetry.utils._compatsequenceDiagram
actor Caller
participant Compat as poetry.utils._compat
participant ImportlibMetadata as importlib.metadata
Caller->>Compat: from poetry.utils._compat import metadata
Compat->>Compat: __getattr__("metadata")
Compat->>Caller: ImportlibMetadata
Caller->>ImportlibMetadata: version("poetry")
ImportlibMetadata-->>Caller: "2.2.1" (example)
Caller->>ImportlibMetadata: (recommended) from importlib import metadata
ImportlibMetadata-->>Caller: metadata module
Class diagram for modules using importlib.metadata after dropping Python 3.9classDiagram
class ImportlibMetadata {
+version(distribution: str) str
}
class CompatModule {
+bool WINDOWS
+decode(data: bytes) str
+encode(text: str) bytes
+getencoding() str
+__getattr__(name: str) object
}
class PoetryVersionModule {
+str __version__
}
class AboutCommand {
+handle() int
}
CompatModule --> ImportlibMetadata : returns metadata
PoetryVersionModule --> ImportlibMetadata : uses for version
AboutCommand --> ImportlibMetadata : uses for metadata
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
Blocking issues:
- Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
General comments:
- Removing
metadatafrompoetry.utils._compatis a behavioral/binary change for anyone importing it directly; consider keeping a shim re-export (or at least a deprecation path) to avoid breaking external users that relied onpoetry.utils._compat.metadata.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Removing `metadata` from `poetry.utils._compat` is a behavioral/binary change for anyone importing it directly; consider keeping a shim re-export (or at least a deprecation path) to avoid breaking external users that relied on `poetry.utils._compat.metadata`.
## Individual Comments
### Comment 1
<location> `src/poetry/utils/env/base_env.py:449-451` </location>
<code_context>
output = subprocess.check_output(
cmd, stderr=stderr, env=env, text=True, encoding="locale", **kwargs
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 2
<location> `src/poetry/utils/env/env_manager.py:287-289` </location>
<code_context>
env_dir = subprocess.check_output(
[python, "-c", GET_ENV_PATH_ONELINER], text=True, encoding="locale"
).strip("\n")
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 3
<location> `src/poetry/utils/env/env_manager.py:331-335` </location>
<code_context>
python_version_string = subprocess.check_output(
[python, "-c", GET_PYTHON_VERSION_ONELINER],
text=True,
encoding="locale",
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 4
<location> `src/poetry/utils/env/base_env.py:453` </location>
<code_context>
def _run(self, cmd: list[str], **kwargs: Any) -> str:
"""
Run a command inside the Python environment.
"""
call = kwargs.pop("call", False)
env = kwargs.pop("env", dict(os.environ))
stderr = kwargs.pop("stderr", subprocess.STDOUT)
try:
if call:
assert stderr != subprocess.PIPE
subprocess.check_call(cmd, stderr=stderr, env=env, **kwargs)
output = ""
else:
output = subprocess.check_output(
cmd, stderr=stderr, env=env, text=True, encoding="locale", **kwargs
)
except CalledProcessError as e:
raise EnvCommandError(e)
return output
</code_context>
<issue_to_address>
**suggestion (code-quality):** Explicitly raise from a previous error ([`raise-from-previous-error`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/raise-from-previous-error/))
```suggestion
raise EnvCommandError(e) from e
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
dc6c998 to
0420f2f
Compare
8db872e to
6e4a5c3
Compare
6e4a5c3 to
4dcd5df
Compare
Member
Author
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
Blocking issues:
- Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
General comments:
- In
poetry/utils/_compat.py,__getattr__should raiseAttributeErrorfor unknown attributes instead of returningNoneto preserve normal module attribute semantics and avoid confusing failures in callers relying on attribute access. - The Perl one-liner in
.github/workflows/.tests-matrix.yamlthat rewritesrequires-pythonnow builds a somewhat convoluted string ("~='${PYTHON_VERSION}'"); double-check the resulting line inpyproject.tomlis syntactically valid and matches the intended version constraint.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `poetry/utils/_compat.py`, `__getattr__` should raise `AttributeError` for unknown attributes instead of returning `None` to preserve normal module attribute semantics and avoid confusing failures in callers relying on attribute access.
- The Perl one-liner in `.github/workflows/.tests-matrix.yaml` that rewrites `requires-python` now builds a somewhat convoluted string (`"~='${PYTHON_VERSION}'"`); double-check the resulting line in `pyproject.toml` is syntactically valid and matches the intended version constraint.
## Individual Comments
### Comment 1
<location> `src/poetry/utils/_compat.py:55-64` </location>
<code_context>
return locale.getencoding()
+def __getattr__(name: str) -> object:
+ if name == "metadata":
+ warnings.warn(
+ "Importing `metadata` from `poetry.utils._compat` is deprecated;"
+ " use `importlib.metadata` directly.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _metadata
+ return None
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Module-level __getattr__ should raise AttributeError for unknown attributes instead of returning None.
Returning `None` here violates the expected module-level `__getattr__` contract and will confuse callers and tools that rely on `AttributeError` (e.g. `hasattr`, `getattr(..., default)`). Please change the final line to `raise AttributeError(name)` so only `"metadata"` is special-cased and all other names behave like normal missing attributes.
</issue_to_address>
### Comment 2
<location> `tests/console/commands/test_about.py:20` </location>
<code_context>
from __future__ import annotations
-from poetry.utils._compat import metadata
+from importlib import metadata
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a dedicated test for the deprecated `poetry.utils._compat.metadata` access path and its deprecation warning.
Since `metadata` is now exposed via `__getattr__` on `poetry.utils._compat` with a `DeprecationWarning`, please add a test that:
1. Imports `metadata` from `poetry.utils._compat`.
2. Uses `pytest.warns` to assert a `DeprecationWarning`.
3. Optionally confirms the object matches `importlib.metadata` (e.g. via `version` or `Distribution`).
This will validate the deprecated access path and protect against regressions in the shim implementation.
Suggested implementation:
```python
def test_about(tester: CommandTester) -> None:
def test_about(tester: CommandTester) -> None:
from importlib import metadata
tester.execute()
def test_deprecated_metadata_access_path() -> None:
import importlib.metadata as importlib_metadata
from poetry.utils import _compat
import pytest
with pytest.warns(DeprecationWarning):
compat_metadata = _compat.metadata
assert compat_metadata is importlib_metadata
```
1. If `pytest` is already imported at the top of `tests/console/commands/test_about.py`, remove the inline `import pytest` inside `test_deprecated_metadata_access_path` and use the existing import instead.
2. If your test suite prefers grouping imports at the top of the file, move `import importlib.metadata as importlib_metadata` and `from poetry.utils import _compat` to the module-level imports and adjust the test accordingly.
</issue_to_address>
### Comment 3
<location> `src/poetry/utils/env/base_env.py:449-451` </location>
<code_context>
output = subprocess.check_output(
cmd, stderr=stderr, env=env, text=True, encoding="locale", **kwargs
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 4
<location> `src/poetry/utils/env/env_manager.py:287-289` </location>
<code_context>
env_dir = subprocess.check_output(
[python, "-c", GET_ENV_PATH_ONELINER], text=True, encoding="locale"
).strip("\n")
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 5
<location> `src/poetry/utils/env/env_manager.py:331-335` </location>
<code_context>
python_version_string = subprocess.check_output(
[python, "-c", GET_PYTHON_VERSION_ONELINER],
text=True,
encoding="locale",
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'check_output' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 6
<location> `src/poetry/utils/env/base_env.py:453` </location>
<code_context>
def _run(self, cmd: list[str], **kwargs: Any) -> str:
"""
Run a command inside the Python environment.
"""
call = kwargs.pop("call", False)
env = kwargs.pop("env", dict(os.environ))
stderr = kwargs.pop("stderr", subprocess.STDOUT)
try:
if call:
assert stderr != subprocess.PIPE
subprocess.check_call(cmd, stderr=stderr, env=env, **kwargs)
output = ""
else:
output = subprocess.check_output(
cmd, stderr=stderr, env=env, text=True, encoding="locale", **kwargs
)
except CalledProcessError as e:
raise EnvCommandError(e)
return output
</code_context>
<issue_to_address>
**suggestion (code-quality):** Explicitly raise from a previous error ([`raise-from-previous-error`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/raise-from-previous-error/))
```suggestion
raise EnvCommandError(e) from e
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
…y plugins; just print a deprecation warning
4dcd5df to
7e7d7bd
Compare
Closed
|
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary by Sourcery
Drop support for Python 3.9 and align codebase, metadata, and CI configuration with Python 3.10+ only.
Enhancements:
Build:
CI:
Tests: