-
Notifications
You must be signed in to change notification settings - Fork 95
feat: provide and use Python version support check #832
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 1 commit
6316d70
a136ad7
45cd647
25225fa
1d567c0
f1dbbb4
e3fd56f
8b1dfb1
4b9208e
5cf8652
7d8b1c7
cda8e27
db92fac
118a7c8
f1c46aa
474ec0d
2083b49
4ea124e
c5949c4
6fc1473
8829e20
bdb6260
7896664
54a5611
2e2990b
35a2074
cd64832
e36414a
b6245ca
3a897ba
814ea63
8dee04b
95f4777
a34ec15
a04e2e6
8b3f337
29896cf
d421f98
ac76f7a
e093a96
187f848
b575a16
ecb7211
3c587cf
fbe0f0b
28b0b32
21c9aec
e24c13d
dbc3a26
d472bae
0550f83
491b695
6338389
05bcf6f
fc224b7
a6c40ce
d0414b2
835df53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
We automatically get the distribution package names under the hood.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,10 @@ | |
| import logging | ||
| import sys | ||
| from typing import Optional | ||
| from ._python_version_support import _flatten_message | ||
| from ._python_version_support import ( | ||
| _flatten_message, | ||
| _get_distribution_and_import_packages, | ||
| ) | ||
|
|
||
| # It is a good practice to alias the Version class for clarity in type hints. | ||
| from packaging.version import parse as parse_version, Version as PackagingVersion | ||
|
|
@@ -57,73 +60,90 @@ def get_dependency_version(dependency_name: str) -> Optional[PackagingVersion]: | |
|
|
||
|
|
||
| def warn_deprecation_for_versions_less_than( | ||
| dependent_package: str, | ||
| dependency_name: str, | ||
| dependent_import_package: str, | ||
| dependency_import_package: str, | ||
|
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. nit: it might be nice if these variables were more distinct. Maybe 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. Done. |
||
| next_supported_version: str, | ||
| message_template: Optional[str] = None, | ||
| ): | ||
| """Issue a deprecation warning for outdated versions of `dependency_name`. | ||
| """Issue any needed deprecation warnings for `dependency_import_package`. | ||
|
|
||
| If `dependency_name` is installed at a version less than | ||
| If `dependency_import_package` is installed at a version less than | ||
| `next_supported_versions`, this issues a warning using either a | ||
| default `message_template` or one provided by the user. The | ||
| default `message_template informs users that they will not receive | ||
| future updates `dependent_package` if `dependency_name` is somehow | ||
| pinned to a version lower than `next_supported_version`. | ||
| future updates `dependent_import_package` if | ||
| `dependency_import_package` is somehow pinned to a version lower | ||
| than `next_supported_version`. | ||
|
|
||
| Args: | ||
| dependent_package: The distribution name of the package that | ||
| needs `dependency_name`. | ||
| dependency_name: The distribution name oft he dependency to check. | ||
| dependent_import_package: The import name of the package that | ||
| needs `dependency_import_package`. | ||
| dependency_import_package: The import name of the dependency to check. | ||
| next_supported_version: The version number below which a deprecation | ||
| warning will be logged. | ||
| message_template: A custom default message template to replace | ||
| the default. This `message_template` is treated as an | ||
| f-string, where the following variables are defined: | ||
| `dependency_name`, `dependent_package`, | ||
| `next_supported_version`, and `version_used`. | ||
| `dependency_import_package`, `dependent_import_package`; | ||
| `dependency_packages` and `dependent_packages`, which contain both the | ||
| distribution and import packages for the dependency and the dependent, | ||
| respectively; and `next_supported_version`, and `version_used`, which | ||
| refer to supported and currently-used versions of the dependency. | ||
|
|
||
| """ | ||
| if not dependent_package or not dependency_name or not next_supported_version: | ||
| if ( | ||
| not dependent_import_package | ||
| or not dependency_import_package | ||
| or not next_supported_version | ||
| ): | ||
| return | ||
| version_used = get_dependency_version(dependency_name) | ||
| version_used = get_dependency_version(dependency_import_package) | ||
| if not version_used: | ||
| return | ||
| if version_used < parse_version(next_supported_version): | ||
| ( | ||
| dependency_packages, | ||
| dependency_distribution_package, | ||
| ) = _get_distribution_and_import_packages(dependency_import_package) | ||
| ( | ||
| dependent_packages, | ||
| dependent_distribution_package, | ||
| ) = _get_distribution_and_import_packages(dependent_import_package) | ||
|
||
| message_template = message_template or _flatten_message( | ||
| """DEPRECATION: Package {dependent_package} depends on | ||
| {dependency_name}, currently installed at version | ||
| """DEPRECATION: Package {dependent_packages} depends on | ||
| {dependency_packages}, currently installed at version | ||
| {version_used.__str__}. Future updates to | ||
| {dependent_package} will require {dependency_name} at | ||
| {dependent_packages} will require {dependency_packages} at | ||
| version {next_supported_version} or higher. Please ensure | ||
| that either (a) your Python environment doesn't pin the | ||
| version of {dependency_name}, so that updates to | ||
| {dependent_package} can require the higher version, or (b) | ||
| you manually update your Python environment to use at | ||
| version of {dependency_packages}, so that updates to | ||
| {dependent_packages} can require the higher version, or | ||
| (b) you manually update your Python environment to use at | ||
| least version {next_supported_version} of | ||
| {dependency_name}.""" | ||
| {dependency_packages}.""" | ||
| ) | ||
|
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. can we make this into a constant? Maybe DEFAULT_PACKAGE_DEPRECATION_TEMPLATE? 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. I would prefer not to because the message template is defined and directly used in one place, and it's closely coupled to the variables provided by the function. I think black-boxing it inside the function, and defining it where it's used, is clearer and more compact than defining a separate constant inside or outside the function. |
||
| logging.warning( | ||
| message_template.format( | ||
| dependent_package=dependent_package, | ||
| dependency_name=dependency_name, | ||
| dependent_import_package=dependent_import_package, | ||
| dependency_import_package=dependency_import_package, | ||
| next_supported_version=next_supported_version, | ||
| version_used=version_used, | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def check_dependency_versions(dependent_package: str): | ||
| def check_dependency_versions(dependent_import_package: str): | ||
| """Bundle checks for all package dependencies. | ||
|
|
||
| This function can be called by all dependents of google.api_core, | ||
| to emit needed deprecation warnings for any of their | ||
| dependencies. The dependencies to check should be updated here. | ||
|
|
||
| Args: | ||
| dependent_package: The distribution name of the calling package, whose | ||
| dependent_import_package: The distribution name of the calling package, whose | ||
| dependencies we're checking. | ||
|
|
||
| """ | ||
| warn_deprecation_for_versions_less_than( | ||
| dependent_package, "protobuf (google.protobuf)", "4.25.8" | ||
| dependent_import_package, "google.protobuf", "4.25.8" | ||
| ) | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,6 +99,44 @@ def _flatten_message(text: str) -> str: | |
| return textwrap.dedent(text).strip().replace("\n", " ") | ||
|
||
|
|
||
|
|
||
| # TODO: Remove once we no longer support Python3.7 | ||
| if sys.version_info < (3, 8): | ||
|
|
||
| def _get_pypi_package_name(module_name): | ||
| """Determine the PyPI package name for a given module name.""" | ||
| return None | ||
|
|
||
| else: | ||
| from importlib import metadata | ||
|
|
||
| def _get_pypi_package_name(module_name): | ||
| """Determine the PyPI package name for a given module name.""" | ||
| try: | ||
| # Get the mapping of modules to distributions | ||
| module_to_distributions = metadata.packages_distributions() | ||
|
|
||
| # Check if the module is found in the mapping | ||
| if module_name in module_to_distributions: | ||
| # The value is a list of distribution names, take the first one | ||
| return module_to_distributions[module_name][0] | ||
| else: | ||
| return None # Module not found in the mapping | ||
| except Exception as e: | ||
| print(f"An error occurred: {e}") | ||
| return None | ||
|
|
||
|
|
||
| def _get_distribution_and_import_packages(import_package: str) -> Optional[str]: | ||
| """Return a pretty string with distribution & import package names.""" | ||
| distribution_package = _get_pypi_package_name(import_package) | ||
| dependency_distribution_and_import_packages = ( | ||
| f"package {distribution_package} ({import_package})" | ||
| if distribution_package | ||
| else import_package | ||
| ) | ||
| return dependency_distribution_and_import_packages, distribution_package | ||
|
|
||
|
|
||
| def check_python_version( | ||
| package: Optional[str] = "this package", today: Optional[datetime.date] = None | ||
| ) -> PythonVersionStatus: | ||
|
|
@@ -111,6 +149,7 @@ def check_python_version( | |
| The support status of the current Python version. | ||
| """ | ||
| today = today or datetime.date.today() | ||
| package_label, _ = _get_distribution_and_import_packages(package) | ||
|
|
||
| python_version = sys.version_info | ||
| version_tuple = (python_version.major, python_version.minor) | ||
|
|
@@ -135,9 +174,7 @@ def check_python_version( | |
| gapic_deprecation = version_info.gapic_deprecation or ( | ||
| version_info.python_eol - DEPRECATION_WARNING_PERIOD | ||
| ) | ||
| gapic_end = version_info.gapic_end or ( | ||
| version_info.python_eol + EOL_GRACE_PERIOD | ||
| ) | ||
| gapic_end = version_info.gapic_end or (version_info.python_eol + EOL_GRACE_PERIOD) | ||
|
|
||
| def min_python(date: datetime.date) -> str: | ||
| """Find the minimum supported Python version for a given date.""" | ||
|
|
@@ -148,42 +185,38 @@ def min_python(date: datetime.date) -> str: | |
|
|
||
| if gapic_end < today: | ||
| message = _flatten_message( | ||
| f""" | ||
| You are using a non-supported Python version ({py_version_str}). | ||
| Google will not post any further updates to {package}. We suggest | ||
| you upgrade to the latest Python version, or at least Python | ||
| {min_python(today)}, and then update {package}. | ||
| """ | ||
| f"""You are using a non-supported Python version | ||
| ({py_version_str}). Google will not post any further | ||
| updates to {package_label}. We suggest you upgrade to the | ||
| latest Python version, or at least Python | ||
| {min_python(today)}, and then update {package_label}. """ | ||
| ) | ||
| logging.warning(message) | ||
| return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED | ||
|
|
||
| eol_date = version_info.python_eol + EOL_GRACE_PERIOD | ||
| if eol_date <= today <= gapic_end: | ||
| message = _flatten_message( | ||
| f""" | ||
| You are using a Python version ({py_version_str}) past its end | ||
| of life. Google will update {package} with critical | ||
| bug fixes on a best-effort basis, but not with any other fixes or | ||
| features. We suggest you upgrade to the latest Python version, | ||
| or at least Python {min_python(today)}, and then update {package}. | ||
| """ | ||
| f"""You are using a Python version ({py_version_str}) | ||
| past its end of life. Google will update {package_label} | ||
| with critical bug fixes on a best-effort basis, but not | ||
| with any other fixes or features. We suggest you upgrade | ||
| to the latest Python version, or at least Python | ||
| {min_python(today)}, and then update {package_label}.""" | ||
| ) | ||
| logging.warning(message) | ||
| return PythonVersionStatus.PYTHON_VERSION_EOL | ||
|
|
||
| if gapic_deprecation <= today <= gapic_end: | ||
| message = _flatten_message( | ||
| f""" | ||
| You are using a Python version ({py_version_str}), | ||
| which Google will stop supporting in {package} when it | ||
| reaches its end of life ({version_info.python_eol}). We | ||
| f"""You are using a Python version ({py_version_str}), | ||
| which Google will stop supporting in {package_label} when | ||
| it reaches its end of life ({version_info.python_eol}). We | ||
| suggest you upgrade to the latest Python version, or at | ||
| least Python {min_python(version_info.python_eol)}, and | ||
| then update {package}. | ||
| """ | ||
| then update {package_label}.""" | ||
| ) | ||
|
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. Do you think these message (or at least templates) would be better as constants, instead of embedded in the function? 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. No, for the same reason as the other case. They are defined and used directly only once, and the substitution strings and the text are closely related to the logic of the function. I think it reads more clearly to have them defined inline, as they are now. |
||
| logging.warning(message) | ||
| return PythonVersionStatus.PYTHON_VERSION_DEPRECATED | ||
|
|
||
| return PythonVersionStatus.PYTHON_VERSION_SUPPORTED | ||
| return PythonVersionStatus.PYTHON_VERSION_SUPPORTED | ||
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.
nit: can this be re-arranged with comments?
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.
And maybe the check methods themselves should be renamed to make it clear that they emit a warning. If I saw it imported in an external package, that part might not be clear from the name. But maybe I'm overthinking it
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.
I did the rearrangement.
I'm not convinced about the renaming. I think "check" conveys the intent more than simply "warn", and "check _and_warn" is too verbose without a lot of benefit. Open to counterarguments..