From d0f156f6954c89da72687a3df8e40fc05de1c00e Mon Sep 17 00:00:00 2001 From: Felipe Alex Hofmann Date: Sat, 6 Apr 2024 11:07:24 -0700 Subject: [PATCH] Add Python 3.12 support (#550) --- .github/workflows/integration.yml | 6 ++--- .github/workflows/lint.yml | 4 +-- .github/workflows/minimum.yml | 6 ++--- .github/workflows/readme.yml | 6 ++--- .github/workflows/unit.yml | 8 +++--- INSTALL.md | 2 +- pyproject.toml | 27 ++++++++++--------- sdmetrics/__init__.py | 10 +++++-- sdmetrics/reports/base_report.py | 6 ++--- sdmetrics/single_table/table_structure.py | 2 +- tasks.py | 2 +- .../test_correlation_similarity.py | 18 ++++++++----- tests/unit/reports/test_base_report.py | 10 +++---- tests/unit/test___init__.py | 14 +++++----- tests/unit/test_visualization.py | 12 ++++----- tox.ini | 2 +- 16 files changed, 75 insertions(+), 60 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 29743b92..e1b506b9 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -10,12 +10,12 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9445c478..29467d6b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,9 +9,9 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies diff --git a/.github/workflows/minimum.yml b/.github/workflows/minimum.yml index e56b5a5e..cfe47c75 100644 --- a/.github/workflows/minimum.yml +++ b/.github/workflows/minimum.yml @@ -10,12 +10,12 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml index f9bbe1ee..005ad0a9 100644 --- a/.github/workflows/readme.yml +++ b/.github/workflows/readme.yml @@ -10,12 +10,12 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: [ubuntu-latest, macos-latest] # skip windows bc rundoc fails steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 288a67ef..ad882ef3 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -10,12 +10,12 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -26,4 +26,4 @@ jobs: run: invoke unit - if: matrix.os == 'ubuntu-latest' && matrix.python-version == 3.8 name: Upload codecov report - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 diff --git a/INSTALL.md b/INSTALL.md index f033a2fd..351e5161 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,7 +2,7 @@ ## Requirements -**SDMetrics** has been developed and tested on [Python 3.8, 3.9, 3.10 and 3.11](https://www.python.org/downloads/) +**SDMetrics** has been developed and tested on [Python 3.8, 3.9, 3.10, 3.11 and 3.12](https://www.python.org/downloads/) Also, although it is not strictly required, the usage of a [virtualenv]( https://virtualenv.pypa.io/en/latest/) is highly recommended in order to avoid diff --git a/pyproject.toml b/pyproject.toml index 20a71629..4992b357 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,25 +12,29 @@ classifiers = [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] keywords = ['sdmetrics', 'sdmetrics', 'SDMetrics'] dynamic = ['version'] license = { text = 'MIT license' } -requires-python = ">=3.8,<3.12" +requires-python = ">=3.8,<3.13" readme = 'README.md' dependencies = [ "numpy>=1.20.0;python_version<'3.10'", - "numpy>=1.23.3;python_version>='3.10'", + "numpy>=1.23.3;python_version>='3.10' and python_version<'3.12'", + "numpy>=1.26.0;python_version>='3.12'", "pandas>=1.1.3;python_version<'3.10'", "pandas>=1.3.4;python_version>='3.10' and python_version<'3.11'", "pandas>=1.5.0;python_version>='3.11'", "scikit-learn>=0.24;python_version<'3.10'", - "scikit-learn>=1.1.3;python_version>='3.10'", + "scikit-learn>=1.1.3;python_version>='3.10' and python_version<'3.12'", + "scikit-learn>=1.3.0;python_version>='3.12'", "scipy>=1.5.4;python_version<'3.10'", - "scipy>=1.9.2;python_version>='3.10'", - 'copulas>=0.10.0', - 'tqdm>=4.15', - 'plotly>=5.10.0', + "scipy>=1.9.2;python_version>='3.10' and python_version<'3.12'", + "scipy>=1.12.0;python_version>='3.12'", + 'copulas @ git+https://github.com/sdv-dev/Copulas@main', + 'tqdm>=4.29', + 'plotly>=5.19.0', ] [project.urls] @@ -46,15 +50,15 @@ sdmetrics = { main = 'sdmetrics.cli.__main__:main' } [project.optional-dependencies] torch = [ "torch>=1.8.0;python_version<'3.10'", - "torch>=1.11.0;python_version>='3.10' and python_version<'3.11'", - "torch>=2.0.0;python_version>='3.11'", + "torch>=2.0.0;python_version>='3.10' and python_version<'3.12'", + "torch>=2.2.0;python_version>='3.12'", ] pomegranate = ['pomegranate>=0.14.1,<0.14.7'] test = [ 'sdmetrics[torch]', 'pytest>=6.2.5,<7', 'pytest-cov>=2.6.0,<3', - 'pytest-rerunfailures>=10', + 'pytest-rerunfailures>=10.3,<15', 'jupyter>=1.0.0,<2', 'rundoc>=0.4.3,<0.5', 'tomli>=2.0.0,<3', @@ -68,7 +72,7 @@ dev = [ 'build>=1.0.0,<2', 'bump-my-version>=0.18.3,<1', 'pip>=9.0.1', - 'watchdog>=0.8.3,<0.11', + 'watchdog>=1.0.1,<5', # style check 'flake8>=3.7.7,<4', @@ -132,7 +136,6 @@ namespaces = false '*.gif' ] 'sdmetrics' = ['demos/*/*.json', 'demos/*/*.csv'] -'tests' = ['*'] [tool.setuptools.exclude-package-data] '*' = [ diff --git a/sdmetrics/__init__.py b/sdmetrics/__init__.py index 37e3a091..12440240 100644 --- a/sdmetrics/__init__.py +++ b/sdmetrics/__init__.py @@ -8,11 +8,11 @@ import sys import warnings as python_warnings +from importlib.metadata import entry_points from operator import attrgetter from types import ModuleType import pandas as pd -from pkg_resources import iter_entry_points from sdmetrics import ( column_pairs, demos, goal, multi_table, reports, single_column, single_table, timeseries) @@ -134,7 +134,13 @@ def _find_addons(): from top_module.addon_module import x """ group = 'sdmetrics_modules' - for entry_point in iter_entry_points(group=group): + try: + eps = entry_points(group=group) + except TypeError: + # Load-time selection requires Python >= 3.10 or importlib_metadata >= 3.6 + eps = entry_points().get(group, []) + + for entry_point in eps: try: addon = entry_point.load() except Exception: # pylint: disable=broad-exception-caught diff --git a/sdmetrics/reports/base_report.py b/sdmetrics/reports/base_report.py index 4028ee79..48601a06 100644 --- a/sdmetrics/reports/base_report.py +++ b/sdmetrics/reports/base_report.py @@ -1,4 +1,5 @@ """Single table base report.""" +import importlib.metadata import pickle import sys import time @@ -9,7 +10,6 @@ import numpy as np import pandas as pd -import pkg_resources import tqdm from sdmetrics.reports.utils import convert_datetime_columns @@ -287,7 +287,7 @@ def save(self, filepath): filepath (str): The path to the file where the report instance will be serialized. """ - self._package_version = pkg_resources.get_distribution('sdmetrics').version + self._package_version = importlib.metadata.version('sdmetrics') with open(filepath, 'wb') as output: pickle.dump(self, output) @@ -304,7 +304,7 @@ def load(cls, filepath): SDMetrics Report: The loaded report instance. """ - current_version = pkg_resources.get_distribution('sdmetrics').version + current_version = importlib.metadata.version('sdmetrics') with open(filepath, 'rb') as f: report = pickle.load(f) diff --git a/sdmetrics/single_table/table_structure.py b/sdmetrics/single_table/table_structure.py index f7c17bf4..34fc4bc3 100644 --- a/sdmetrics/single_table/table_structure.py +++ b/sdmetrics/single_table/table_structure.py @@ -39,7 +39,7 @@ def compute_breakdown(cls, real_data, synthetic_data): real_columns = set(real_data.columns) intersection_columns = real_columns & synthetic_columns union_columns = real_columns | synthetic_columns - score = len(intersection_columns)/len(union_columns) + score = len(intersection_columns) / len(union_columns) return {'score': score} diff --git a/tasks.py b/tasks.py index 3fee822c..3177da75 100644 --- a/tasks.py +++ b/tasks.py @@ -44,7 +44,7 @@ def _get_minimum_versions(dependencies, python_version): for dependency in dependencies: if '@' in dependency: name, url = dependency.split(' @ ') - min_versions[name] = f'{name} @ {url}' + min_versions[name] = f'{url}#egg={name}' continue req = Requirement(dependency) diff --git a/tests/unit/column_pairs/statistical/test_correlation_similarity.py b/tests/unit/column_pairs/statistical/test_correlation_similarity.py index e15623b5..d950a34e 100644 --- a/tests/unit/column_pairs/statistical/test_correlation_similarity.py +++ b/tests/unit/column_pairs/statistical/test_correlation_similarity.py @@ -44,10 +44,10 @@ def test_compute_breakdown(self, pearson_mock): result = metric.compute_breakdown(real_data, synthetic_data, coefficient='Pearson') # Assert - assert pearson_mock.has_calls( + pearson_mock.assert_has_calls([ call(SeriesMatcher(real_data['col1']), SeriesMatcher(real_data['col2'])), call(SeriesMatcher(synthetic_data['col1']), SeriesMatcher(synthetic_data['col2'])), - ) + ]) assert result == expected_score_breakdown @patch('sdmetrics.column_pairs.statistical.correlation_similarity.pearsonr') @@ -89,10 +89,16 @@ def test_compute_breakdown_datetime(self, pearson_mock): result = metric.compute_breakdown(real_data, synthetic_data, coefficient='Pearson') # Assert - assert pearson_mock.has_calls( - call(SeriesMatcher(real_data['col1']), SeriesMatcher(real_data['col2'])), - call(SeriesMatcher(synthetic_data['col1']), SeriesMatcher(synthetic_data['col2'])), - ) + pearson_mock.assert_has_calls([ + call( + SeriesMatcher( + real_data['col1'].astype('int64')), SeriesMatcher( + real_data['col2'].astype('int64'))), + call( + SeriesMatcher( + synthetic_data['col1'].astype('int64')), SeriesMatcher( + synthetic_data['col2'].astype('int64'))), + ]) assert result == expected_score_breakdown def test_compute_breakdown_constant_input(self): diff --git a/tests/unit/reports/test_base_report.py b/tests/unit/reports/test_base_report.py index 1b821442..cd01b4bb 100644 --- a/tests/unit/reports/test_base_report.py +++ b/tests/unit/reports/test_base_report.py @@ -669,7 +669,7 @@ def test_get_details(self): base_report._properties['Property 1'].details.copy.assert_called_once() base_report._properties['Property 2'].details.copy.assert_called_once() - @patch('sdmetrics.reports.base_report.pkg_resources.get_distribution') + @patch('sdmetrics.reports.base_report.importlib.metadata.version') @patch('sdmetrics.reports.base_report.pickle') def test_save(self, pickle_mock, get_distribution_mock): """Test the ``save`` method. @@ -694,9 +694,9 @@ def test_save(self, pickle_mock, get_distribution_mock): get_distribution_mock.assert_called_once_with('sdmetrics') open_mock.assert_called_once_with('test-file.pkl', 'wb') pickle_mock.dump.assert_called_once_with(report, open_mock()) - assert report._package_version == get_distribution_mock.return_value.version + assert report._package_version == get_distribution_mock.return_value - @patch('sdmetrics.reports.base_report.pkg_resources.get_distribution') + @patch('sdmetrics.reports.base_report.importlib.metadata.version') @patch('sdmetrics.reports.base_report.pickle') def test_load(self, pickle_mock, get_distribution_mock): """Test the ``load`` method. @@ -727,7 +727,7 @@ def test_load(self, pickle_mock, get_distribution_mock): assert loaded == pickle_mock.load.return_value @patch('sdmetrics.reports.base_report.warnings') - @patch('sdmetrics.reports.base_report.pkg_resources.get_distribution') + @patch('sdmetrics.reports.base_report.importlib.metadata.version') @patch('sdmetrics.reports.base_report.pickle') def test_load_mismatched_versions(self, pickle_mock, get_distribution_mock, warnings_mock): """Test the ``load`` method with mismatched sdmetrics versions. @@ -748,7 +748,7 @@ def test_load_mismatched_versions(self, pickle_mock, get_distribution_mock, warn report = Mock() pickle_mock.load.return_value = report report._package_version = 'previous_version' - get_distribution_mock.return_value.version = 'new_version' + get_distribution_mock.return_value = 'new_version' # Run with patch('sdmetrics.reports.base_report.open', open_mock): diff --git a/tests/unit/test___init__.py b/tests/unit/test___init__.py index e4531998..10b9b8d7 100644 --- a/tests/unit/test___init__.py +++ b/tests/unit/test___init__.py @@ -18,7 +18,7 @@ def mock_sdmetrics(): sys.modules['sdmetrics'] = sdmetrics_module -@patch.object(sdmetrics, 'iter_entry_points') +@patch.object(sdmetrics, 'entry_points') def test__find_addons_module(entry_points_mock, mock_sdmetrics): """Test loading an add-on.""" # Setup @@ -37,7 +37,7 @@ def test__find_addons_module(entry_points_mock, mock_sdmetrics): assert sys.modules['sdmetrics.submodule.entry_name'] == add_on_mock -@patch.object(sdmetrics, 'iter_entry_points') +@patch.object(sdmetrics, 'entry_points') def test__find_addons_object(entry_points_mock, mock_sdmetrics): """Test loading an add-on.""" # Setup @@ -55,7 +55,7 @@ def test__find_addons_object(entry_points_mock, mock_sdmetrics): @patch('warnings.warn') -@patch('sdmetrics.iter_entry_points') +@patch('sdmetrics.entry_points') def test__find_addons_bad_addon(entry_points_mock, warning_mock): """Test failing to load an add-on generates a warning.""" # Setup @@ -78,7 +78,7 @@ def entry_point_error(): @patch('warnings.warn') -@patch('sdmetrics.iter_entry_points') +@patch('sdmetrics.entry_points') def test__find_addons_wrong_base(entry_points_mock, warning_mock): """Test incorrect add-on name generates a warning.""" # Setup @@ -99,7 +99,7 @@ def test__find_addons_wrong_base(entry_points_mock, warning_mock): @patch('warnings.warn') -@patch('sdmetrics.iter_entry_points') +@patch('sdmetrics.entry_points') def test__find_addons_missing_submodule(entry_points_mock, warning_mock): """Test incorrect add-on name generates a warning.""" # Setup @@ -120,7 +120,7 @@ def test__find_addons_missing_submodule(entry_points_mock, warning_mock): @patch('warnings.warn') -@patch('sdmetrics.iter_entry_points') +@patch('sdmetrics.entry_points') def test__find_addons_module_and_object(entry_points_mock, warning_mock): """Test incorrect add-on name generates a warning.""" # Setup @@ -141,7 +141,7 @@ def test__find_addons_module_and_object(entry_points_mock, warning_mock): @patch('warnings.warn') -@patch.object(sdmetrics, 'iter_entry_points') +@patch.object(sdmetrics, 'entry_points') def test__find_addons_missing_object(entry_points_mock, warning_mock, mock_sdmetrics): """Test incorrect add-on name generates a warning.""" # Setup diff --git a/tests/unit/test_visualization.py b/tests/unit/test_visualization.py index 6f98a54e..70d73ad3 100644 --- a/tests/unit/test_visualization.py +++ b/tests/unit/test_visualization.py @@ -412,7 +412,7 @@ def test__generate_scatter_plot(px_mock): color_discrete_map={'Real': '#000036', 'Synthetic': '#01E0C9'}, symbol='Data', ) - assert mock_figure.update_layout.called_once() + mock_figure.update_layout.assert_called_once() assert fig == mock_figure @@ -460,8 +460,8 @@ def test__generate_heatmap_plot(px_mock): facet_col='Data', histnorm='probability', ) - assert mock_figure.update_layout.called_once() - assert mock_figure.for_each_annotation.called_once() + mock_figure.update_layout.assert_called_once() + mock_figure.for_each_annotation.assert_called_once() assert fig == mock_figure @@ -508,8 +508,8 @@ def test__generate_line_plot(px_mock): color='Data', color_discrete_map={'Real': '#000036', 'Synthetic': '#01E0C9'} ) - assert mock_figure.update_layout.called_once() - assert mock_figure.for_each_annotation.called_once() + mock_figure.update_layout.assert_called_once() + mock_figure.for_each_annotation.assert_not_called() assert fig == mock_figure # Setup failing case sequence index @@ -581,7 +581,7 @@ def test__generate_box_plot(px_mock): color='Data', color_discrete_map={'Real': '#000036', 'Synthetic': '#01E0C9'}, ) - assert mock_figure.update_layout.called_once() + mock_figure.update_layout.assert_called_once() assert fig == mock_figure diff --git a/tox.ini b/tox.ini index f1051802..c4f3feab 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39-lint, py3{8,9,10,11}-{readme,integration,unit,minimum} +envlist = py39-lint, py3{8,9,10,11,12}-{readme,integration,unit,minimum} [testenv] skipsdist = false