diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml index b3a44130..03a14ac5 100644 --- a/.github/workflows/readme.yml +++ b/.github/workflows/readme.yml @@ -22,5 +22,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install invoke rundoc . + python -m pip install tomli + python -m pip install packaging - name: Run the README.md run: invoke readme diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 893eb130..ca9fdba2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -219,9 +219,9 @@ This will perform the following actions: 2. Bump the current version to the next release candidate, ``X.Y.Z.dev(N+1)`` After this is done, the new pre-release can be installed by including the ``dev`` section in the -dependency specification, either in ``setup.py``:: +dependency specification, either in ``pyproject.toml``:: - install_requires = [ + dependencies = [ ... 'ctgan>=X.Y.Z.dev', ... diff --git a/Makefile b/Makefile index a3e3db94..3854b885 100644 --- a/Makefile +++ b/Makefile @@ -76,16 +76,9 @@ install-test: clean-build clean-pyc ## install the package and test dependencies install-develop: clean-build clean-pyc ## install the package in editable mode and dependencies for development pip install -e .[dev] -MINIMUM := $(shell sed -n '/install_requires = \[/,/]/p' setup.py | head -n-1 | tail -n+2 | sed 's/ *\(.*\),$?$$/\1/g' | tr '>' '=') - -.PHONY: install-minimum -install-minimum: ## install the minimum supported versions of the package dependencies - pip install $(MINIMUM) - # LINT TARGETS - .PHONY: lint lint: ## check style with flake8 and isort invoke lint @@ -138,8 +131,7 @@ coverage: ## check code coverage quickly with the default Python .PHONY: dist dist: clean ## builds source and wheel package - python setup.py sdist - python setup.py bdist_wheel + python -m build --wheel --sdist ls -l dist .PHONY: publish-confirm diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..ffe9cf07 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,126 @@ +[build-system] +requires = ['setuptools', 'wheel'] +build-backend = 'setuptools.build_meta' + +[project] +name = 'ctgan' +description = 'Create tabular synthetic data using a conditional GAN' +authors = [{ name = 'DataCebo, Inc.', email = 'info@sdv.dev' }] +classifiers = [ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', + 'License :: Free for non-commercial use', + 'Natural Language :: English', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Topic :: Scientific/Engineering :: Artificial Intelligence', +] +keywords = ['ctgan', 'CTGAN'] +version = '0.9.1.dev0' +license = { text = 'BSL-1.1' } +requires-python = '>=3.8,<3.12' +readme = 'README.md' +dependencies = [ + "numpy>=1.20.0;python_version<'3.10'", + "numpy>=1.23.3;python_version>='3.10'", + "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'", + "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'", + 'tqdm>=4.15', + 'rdt>=1.6.1', +] + +[project.urls] +"Source Code"= "https://github.com/sdv-dev/CTGAN/" +"Issue Tracker" = "https://github.com/sdv-dev/CTGAN/issues" +"Changes" = "https://github.com/sdv-dev/CTGAN/blob/main/HISTORY.md" +"Twitter" = "https://twitter.com/sdv_dev" +"Chat" = "https://bit.ly/sdv-slack-invite" + +[project.entry-points] +ctgan = { main = 'ctgan.cli.__main__:main' } + +[project.optional-dependencies] +test = [ + 'pytest>=3.4.2', + 'pytest-rerunfailures>=9.1.1,<10', + 'pytest-cov>=2.6.0', + 'rundoc>=0.4.3,<0.5', + 'pytest-runner >= 2.11.1', + 'tomli>=2.0.0,<3', +] +dev = [ + 'ctgan[test]', + + # general + 'pip>=9.0.1', + 'bumpversion>=0.5.3,<0.6', + 'watchdog>=0.8.3,<0.11', + + # style check + 'flake8>=3.7.7,<4', + 'isort>=4.3.4,<5', + 'dlint>=0.11.0,<0.12', # code security addon for flake8 + 'flake8-debugger>=4.0.0,<4.1', + 'flake8-mock>=0.3,<0.4', + 'flake8-mutable>=1.2.0,<1.3', + 'flake8-absolute-import>=1.0,<2', + 'flake8-multiline-containers>=0.0.18,<0.1', + 'flake8-print>=4.0.0,<4.1', + 'flake8-quotes>=3.3.0,<4', + 'flake8-fixme>=1.1.1,<1.2', + 'flake8-expression-complexity>=0.0.9,<0.1', + 'flake8-eradicate>=1.1.0,<1.2', + 'flake8-builtins>=1.5.3,<1.6', + 'flake8-variables-names>=0.0.4,<0.1', + 'pandas-vet>=0.2.2,<0.3', + 'flake8-comprehensions>=3.6.1,<3.7', + 'dlint>=0.11.0,<0.12', + 'flake8-docstrings>=1.5.0,<2', + 'flake8-sfs>=0.0.3,<0.1', + 'flake8-pytest-style>=1.5.0,<2', + + # fix style issues + 'autoflake>=1.1,<2', + 'autopep8>=1.4.3,<1.6', + + # distribute on PyPI + 'twine>=1.10.0,<4', + 'wheel>=0.30.0', + + # Advanced testing + 'coverage>=4.5.1,<6', + 'tox>=2.9.1,<4', + + 'invoke', +] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = ['ctgan', 'ctgan.*'] +namespaces = false + +[tool.isort] +include_trailing_comment = true +line_length = 99 +lines_between_types = 0 +multi_line_output = 4 +not_skip = ['__init__.py'] +use_parentheses = true + +[tool.pydocstyle] +convention = 'google' +add-ignore = ['D107', 'D407', 'D417'] + +[tool.pytest.ini_options] +collect_ignore = ['pyproject.toml'] diff --git a/setup.cfg b/setup.cfg index 29a8871a..0b264aa1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ values = [bumpversion:part:candidate] -[bumpversion:file:setup.py] +[bumpversion:file:pyproject.toml] search = version='{current_version}' replace = version='{new_version}' @@ -39,17 +39,6 @@ extend-ignore = D107, # Missing docstring in __init__ per-file-ignores = ctgan/data.py:T001 -[isort] -include_trailing_comment = True -line_length = 99 -lines_between_types = 0 -multi_line_output = 4 -not_skip = __init__.py -use_parentheses = True - [aliases] test = pytest -[tool:pytest] -collect_ignore = ['setup.py'] - diff --git a/setup.py b/setup.py deleted file mode 100644 index 545f5273..00000000 --- a/setup.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""The setup script.""" - -from setuptools import find_packages, setup - -with open('README.md', encoding='utf-8') as readme_file: - readme = readme_file.read() - -with open('HISTORY.md', encoding='utf-8') as history_file: - history = history_file.read() - -install_requires = [ - "numpy>=1.20.0;python_version<'3.10'", - "numpy>=1.23.3;python_version>='3.10'", - "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>=1.1.3;python_version>='3.10'", - "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'", - 'tqdm>=4.15', - 'rdt>=1.6.1', -] - -setup_requires = [ - 'pytest-runner>=2.11.1', -] - -tests_require = [ - 'pytest>=3.4.2', - 'pytest-rerunfailures>=9.1.1,<10', - 'pytest-cov>=2.6.0', - 'rundoc>=0.4.3,<0.5', -] - -development_requires = [ - # general - 'pip>=9.0.1', - 'bumpversion>=0.5.3,<0.6', - 'watchdog>=0.8.3,<0.11', - - # style check - 'flake8>=3.7.7,<4', - 'isort>=4.3.4,<5', - 'dlint>=0.11.0,<0.12', # code security addon for flake8 - 'flake8-debugger>=4.0.0,<4.1', - 'flake8-mock>=0.3,<0.4', - 'flake8-mutable>=1.2.0,<1.3', - 'flake8-absolute-import>=1.0,<2', - 'flake8-multiline-containers>=0.0.18,<0.1', - 'flake8-print>=4.0.0,<4.1', - 'flake8-quotes>=3.3.0,<4', - 'flake8-fixme>=1.1.1,<1.2', - 'flake8-expression-complexity>=0.0.9,<0.1', - 'flake8-eradicate>=1.1.0,<1.2', - 'flake8-builtins>=1.5.3,<1.6', - 'flake8-variables-names>=0.0.4,<0.1', - 'pandas-vet>=0.2.2,<0.3', - 'flake8-comprehensions>=3.6.1,<3.7', - 'dlint>=0.11.0,<0.12', - 'flake8-docstrings>=1.5.0,<2', - 'flake8-sfs>=0.0.3,<0.1', - 'flake8-pytest-style>=1.5.0,<2', - - # fix style issues - 'autoflake>=1.1,<2', - 'autopep8>=1.4.3,<1.6', - - # distribute on PyPI - 'twine>=1.10.0,<4', - 'wheel>=0.30.0', - - # Advanced testing - 'coverage>=4.5.1,<6', - 'tox>=2.9.1,<4', - - 'invoke', -] - -setup( - author='DataCebo, Inc.', - author_email='info@sdv.dev', - classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Developers', - 'License :: Free for non-commercial use', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Topic :: Scientific/Engineering :: Artificial Intelligence', - ], - description='Create tabular synthetic data using a conditional GAN', - entry_points={ - 'console_scripts': [ - 'ctgan=ctgan.__main__:main' - ], - }, - extras_require={ - 'test': tests_require, - 'dev': development_requires + tests_require, - }, - install_package_data=True, - install_requires=install_requires, - license='BSL-1.1', - long_description=readme + '\n\n' + history, - long_description_content_type='text/markdown', - include_package_data=True, - keywords='ctgan CTGAN', - name='ctgan', - packages=find_packages(include=['ctgan', 'ctgan.*']), - python_requires='>=3.8,<3.12', - setup_requires=setup_requires, - test_suite='tests', - tests_require=tests_require, - url='https://github.com/sdv-dev/CTGAN', - version='0.9.1.dev0', - zip_safe=False, -) diff --git a/tasks.py b/tasks.py index f5782f0f..26adfd76 100644 --- a/tasks.py +++ b/tasks.py @@ -1,15 +1,15 @@ -import glob import inspect import operator import os -import re -import pkg_resources -import platform import shutil import stat +import sys from pathlib import Path +import tomli from invoke import task +from packaging.requirements import Requirement +from packaging.version import Version COMPARISONS = { '>=': operator.ge, @@ -52,49 +52,45 @@ def readme(c): shutil.rmtree(test_path) -def _validate_python_version(line): - is_valid = True - for python_version_match in re.finditer(r"python_version(<=?|>=?|==)\'(\d\.?)+\'", line): - python_version = python_version_match.group(0) - comparison = re.search(r'(>=?|<=?|==)', python_version).group(0) - version_number = python_version.split(comparison)[-1].replace("'", "") - comparison_function = COMPARISONS[comparison] - is_valid = is_valid and comparison_function( - pkg_resources.parse_version(platform.python_version()), - pkg_resources.parse_version(version_number), - ) +def _get_minimum_versions(dependencies, python_version): + min_versions = {} + for dependency in dependencies: + if '@' in dependency: + name, url = dependency.split(' @ ') + min_versions[name] = f'{name} @ {url}' + continue - return is_valid + req = Requirement(dependency) + if ';' in dependency: + marker = req.marker + if marker and not marker.evaluate({'python_version': python_version}): + continue # Skip this dependency if the marker does not apply to the current Python version + + if req.name not in min_versions: + min_version = next((spec.version for spec in req.specifier if spec.operator in ('>=', '==')), None) + if min_version: + min_versions[req.name] = f'{req.name}=={min_version}' + + elif '@' not in min_versions[req.name]: + existing_version = Version(min_versions[req.name].split('==')[1]) + new_version = next((spec.version for spec in req.specifier if spec.operator in ('>=', '==')), existing_version) + if new_version > existing_version: + min_versions[req.name] = f'{req.name}=={new_version}' # Change when a valid newer version is found + + return list(min_versions.values()) @task def install_minimum(c): - with open('setup.py', 'r') as setup_py: - lines = setup_py.read().splitlines() - - versions = [] - started = False - for line in lines: - if started: - if line == ']': - break - - line = line.strip() - if _validate_python_version(line): - requirement = re.match(r'[^>]*', line).group(0) - requirement = re.sub(r"""['",]""", '', requirement) - version = re.search(r'>=?(\d\.?)+', line).group(0) - if version: - version = re.sub(r'>=?', '==', version) - version = re.sub(r"""['",]""", '', version) - requirement += version - - versions.append(requirement) - - elif line.startswith('install_requires = ['): - started = True - - c.run(f'python -m pip install {" ".join(versions)}') + with open('pyproject.toml', 'rb') as pyproject_file: + pyproject_data = tomli.load(pyproject_file) + + dependencies = pyproject_data.get('project', {}).get('dependencies', []) + python_version = '.'.join(map(str, sys.version_info[:2])) + minimum_versions = _get_minimum_versions(dependencies, python_version) + + if minimum_versions: + c.run(f'python -m pip install {" ".join(minimum_versions)}') @task diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..28045a5d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""CTGAN tests.""" diff --git a/tests/test_tasks.py b/tests/test_tasks.py new file mode 100644 index 00000000..c78986cf --- /dev/null +++ b/tests/test_tasks.py @@ -0,0 +1,38 @@ +"""Tests for the ``tasks.py`` file.""" +from tasks import _get_minimum_versions + + +def test_get_minimum_versions(): + """Test the ``_get_minimum_versions`` method. + + The method should return the minimum versions of the dependencies for the given python version. + If a library is linked to an URL, the minimum version should be the URL. + """ + # Setup + dependencies = [ + "numpy>=1.20.0,<2;python_version<'3.10'", + "numpy>=1.23.3,<2;python_version>='3.10'", + "pandas>=1.2.0,<2;python_version<'3.10'", + "pandas>=1.3.0,<2;python_version>='3.10'", + 'humanfriendly>=8.2,<11', + 'pandas @ git+https://github.com/pandas-dev/pandas.git@master#egg=pandas' + ] + + # Run + minimum_versions_39 = _get_minimum_versions(dependencies, '3.9') + minimum_versions_310 = _get_minimum_versions(dependencies, '3.10') + + # Assert + expected_versions_39 = [ + 'numpy==1.20.0', + 'pandas @ git+https://github.com/pandas-dev/pandas.git@master#egg=pandas', + 'humanfriendly==8.2', + ] + expected_versions_310 = [ + 'numpy==1.23.3', + 'pandas @ git+https://github.com/pandas-dev/pandas.git@master#egg=pandas', + 'humanfriendly==8.2', + ] + + assert minimum_versions_39 == expected_versions_39 + assert minimum_versions_310 == expected_versions_310