diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d2b7f71..e11b65ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +fix: `Client._get_version()` raised `UnboundLocalError` instead of falling back when the package metadata was missing, which broke every request from an uninstalled source checkout ## [2.0.1][2.0.1] - 2026-03-09 feat: Added Support for cancel token diff --git a/razorpay/client.py b/razorpay/client.py index 6d80e526..2071686d 100644 --- a/razorpay/client.py +++ b/razorpay/client.py @@ -97,33 +97,32 @@ def _update_user_agent_header(self, options): return options def _get_version(self): - version = "" - try: # nosemgrep : gitlab.bandit.B110 - # Try importlib.metadata first (modern approach) - try: - import importlib.metadata - from importlib.metadata import PackageNotFoundError - version = importlib.metadata.version("razorpay") - except ImportError: - # Fall back to pkg_resources + """ + Version of the installed razorpay package, used in the User-Agent. + Falls back to a fixed value, with a warning, when the package + metadata cannot be found (for example when running from a source + checkout that was never installed). + """ + try: + from importlib.metadata import PackageNotFoundError + from importlib.metadata import version as package_version + except ImportError: # pragma: no cover - Python < 3.8 + from pkg_resources import DistributionNotFound as PackageNotFoundError + + def package_version(name): import pkg_resources - from pkg_resources import DistributionNotFound - version = pkg_resources.require("razorpay")[0].version - except (PackageNotFoundError, DistributionNotFound, NameError): # pragma: no cover - # PackageNotFoundError: importlib.metadata couldn't find the package - # DistributionNotFound: pkg_resources couldn't find the package - # NameError: in case the exception classes aren't defined due to import issues - - # If all else fails, use the hardcoded version from the package - version = "1.4.3" + return pkg_resources.require(name)[0].version + try: + return package_version("razorpay") + except PackageNotFoundError: warnings.warn( - "Could not detect razorpay package version. Using fallback version." - "This may indicate an installation issue.", - UserWarning, - stacklevel=4 + "Could not detect razorpay package version. Using fallback version. " + "This may indicate an installation issue.", + UserWarning, + stacklevel=4 ) - return version + return "1.4.3" def _get_app_details_ua(self): app_details_ua = "" diff --git a/tests/test_client_version.py b/tests/test_client_version.py new file mode 100644 index 00000000..040ac51e --- /dev/null +++ b/tests/test_client_version.py @@ -0,0 +1,40 @@ +import importlib.metadata +import unittest +import warnings +from unittest import mock + +import razorpay + + +class TestClientVersion(unittest.TestCase): + + def setUp(self): + self.client = razorpay.Client(auth=('key_id', 'key_secret')) + + def test_version_comes_from_package_metadata(self): + self.assertEqual(self.client._get_version(), + importlib.metadata.version('razorpay')) + + def test_missing_metadata_falls_back_with_a_warning(self): + def missing(name): + raise importlib.metadata.PackageNotFoundError(name) + + with mock.patch('importlib.metadata.version', missing): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + version = self.client._get_version() + + self.assertTrue(version) + self.assertTrue(any(issubclass(w.category, UserWarning) for w in caught), + 'a UserWarning should explain the fallback') + + def test_requests_still_work_without_package_metadata(self): + def missing(name): + raise importlib.metadata.PackageNotFoundError(name) + + with mock.patch('importlib.metadata.version', missing): + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + options = self.client._update_user_agent_header({}) + + self.assertIn('Razorpay-Python/', options['headers']['User-Agent'])