diff --git a/.gitattributes b/.gitattributes index ade44ab7c..040321c04 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ tableauserverclient/_version.py export-subst +tableauserverclient/bin/_version.py export-subst diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 3bbecd3c2..d6d36f7ba 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,9 +1,12 @@ name: Publish to PyPi -# This will publish a package to TestPyPi (and real Pypi if run on master) with a version -# number generated by versioneer from the most recent tag looking like v____ -# TODO: maybe move this into the package job so all release-based actions are together +# This will build a package with a version set by versioneer from the most recent tag matching v____ +# It will publish to TestPyPi, and to real Pypi *if* run on master where head has a release tag +# For a live run, this should only need to be triggered by a newly published repo release. +# This can also be run manually for testing on: + release: + types: [published] workflow_dispatch: push: tags: @@ -23,7 +26,7 @@ jobs: - name: Build dist files run: | python -m pip install --upgrade pip - pip install -e .[test] build + python -m pip install -e .[test] build python -m build git describe --tag --dirty --always diff --git a/MANIFEST.in b/MANIFEST.in index 9b7512fb9..7acbed103 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,8 +4,6 @@ include CONTRIBUTORS.md include LICENSE include LICENSE.versioneer include README.md -include tableauserverclient/_version.py -include versioneer.py recursive-include docs *.md recursive-include samples *.py recursive-include samples *.txt @@ -18,5 +16,4 @@ recursive-include test *.png recursive-include test *.py recursive-include test *.xml recursive-include test *.tde -global-include *.pyi global-include *.typed diff --git a/publish.sh b/publish.sh deleted file mode 100755 index 46d54a1ee..000000000 --- a/publish.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# tag the release version and confirm a clean version number -git tag vxxxx -git describe --tag --dirty --always - -set -e - -rm -rf dist -python setup.py sdist bdist_wheel -twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml index 857f3b7ab..22760e803 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=75.0", "versioneer[toml]==0.29", "wheel"] +requires = ["setuptools>=77.0", "versioneer[toml]==0.29", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,7 +8,7 @@ name="tableauserverclient" dynamic = ["version"] description='A Python module for working with the Tableau Server REST API.' authors = [{name="Tableau", email="github@tableau.com"}] -license = {file = "LICENSE"} +license-files = ["LICENSE"] readme = "README.md" dependencies = [ @@ -34,6 +34,13 @@ repository = "https://github.com/tableau/server-client-python" [project.optional-dependencies] test = ["black==24.10", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests", "pytest-xdist", "requests-mock>=1.0,<2.0", "types-requests>=2.32.4.20250913"] + +[tool.setuptools.packages.find] +where = ["tableauserverclient", "tableauserverclient.helpers", "tableauserverclient.models", "tableauserverclient.server", "tableauserverclient.server.endpoint"] + +[tool.setuptools.dynamic] +version = {attr = "versioneer.get_version"} + [tool.black] line-length = 120 target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] @@ -60,5 +67,5 @@ addopts = "--junitxml=./test.junit.xml" VCS = "git" style = "pep440-pre" versionfile_source = "tableauserverclient/bin/_version.py" -versionfile_build = "tableauserverclient/bin/_version.py" +versionfile_build = "_version.py" tag_prefix = "v" diff --git a/setup.py b/setup.py index bdce51f2e..b52ba267e 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,7 @@ from setuptools import setup setup( - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), - # not yet sure how to move this to pyproject.toml - packages=[ - "tableauserverclient", - "tableauserverclient.helpers", - "tableauserverclient.models", - "tableauserverclient.server", - "tableauserverclient.server.endpoint", - ], + # This line is required to set the version number when building the wheel + # not yet sure how to move this to pyproject.toml - it may require work in versioneer + cmdclass=versioneer.get_cmdclass() ) diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index cd0ec3e03..7241f23ca 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -148,7 +148,3 @@ "WeeklyInterval", "WorkbookItem", ] - -from .bin import _version - -__version__ = _version.get_versions()["version"] diff --git a/tableauserverclient/bin/__init__.py b/tableauserverclient/bin/__init__.py new file mode 100644 index 000000000..e4605a43b --- /dev/null +++ b/tableauserverclient/bin/__init__.py @@ -0,0 +1,3 @@ +# generated during initial setup of versioneer +from . import _version +__version__ = _version.get_versions()['version'] diff --git a/tableauserverclient/bin/_version.py b/tableauserverclient/bin/_version.py index f23819e86..680304c7d 100644 --- a/tableauserverclient/bin/_version.py +++ b/tableauserverclient/bin/_version.py @@ -46,14 +46,13 @@ class VersioneerConfig: def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py + # these strings are filled in from pyproject.toml at file generation time cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "pep440-pre" cfg.tag_prefix = "v" cfg.parentdir_prefix = "None" - cfg.versionfile_source = "tableauserverclient/_version.py" + cfg.versionfile_source = "tableauserverclient/bin/_version.py" cfg.verbose = False return cfg diff --git a/test/http/__init__.py b/test/http/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/models/__init__.py b/test/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/models/_models.py b/test/models/_models.py index 59011c6c3..9be97a87b 100644 --- a/test/models/_models.py +++ b/test/models/_models.py @@ -10,48 +10,27 @@ ) -def get_defined_models(): - # nothing clever here: list was manually copied from tsc/models/__init__.py - return [ - BackgroundJobItem, - ConnectionItem, - DataAccelerationReportItem, - DataAlertItem, - DatasourceItem, - FlowItem, - GroupItem, - JobItem, - MetricItem, - PermissionsRule, - ProjectItem, - RevisionItem, - ScheduleItem, - SubscriptionItem, - Credentials, - JWTAuth, - TableauAuth, - PersonalAccessTokenAuth, - ServerInfoItem, - SiteItem, - TaskItem, - UserItem, - ViewItem, - WebhookItem, - WorkbookItem, - PaginationItem, - Permission.Mode, - Permission.Capability, - DailyInterval, - WeeklyInterval, - MonthlyInterval, - HourlyInterval, - TableItem, - Target, - ] - - def get_unimplemented_models(): return [ + # these items should have repr , please fix + CollectionItem, + DQWItem, + ExtensionsServer, + ExtensionsSiteSettings, + FileuploadItem, + FlowRunItem, + LinkedTaskFlowRunItem, + LinkedTaskItem, + LinkedTaskStepItem, + SafeExtension, + # these should be implemented together for consistency + CSVRequestOptions, + ExcelRequestOptions, + ImageRequestOptions, + PDFRequestOptions, + PPTXRequestOptions, + RequestOptions, + # these don't need it FavoriteItem, # no repr because there is no state Resource, # list of type names TableauItem, # should be an interface diff --git a/test/models/test_repr.py b/test/models/test_repr.py index 0f6057f4f..34f8509a7 100644 --- a/test/models/test_repr.py +++ b/test/models/test_repr.py @@ -1,15 +1,30 @@ import inspect from typing import Any - -import _models # type: ignore # did not set types for this +from test.models._models import get_unimplemented_models import tableauserverclient as TSC import pytest -# ensure that all models that don't need parameters can be instantiated -# todo.... -def instantiate_class(name: str, obj: Any): +def is_concrete(obj: Any): + return inspect.isclass(obj) and not inspect.isabstract(obj) + + +@pytest.mark.parametrize("class_name, obj", inspect.getmembers(TSC, is_concrete)) +def test_by_reflection(class_name, obj): + instance = try_instantiate_class(class_name, obj) + if instance: + class_type = type(instance) + if class_type in get_unimplemented_models(): + print(f"Class '{class_name}' has no repr defined, skipping test") + return + else: + assert type(instance.__repr__).__name__ == "method" + print(instance.__repr__.__name__) + + +# Instantiate a class if it doesn't require any parameters +def try_instantiate_class(name: str, obj: Any) -> Any | None: # Get the constructor (init) of the class constructor = getattr(obj, "__init__", None) if constructor: @@ -22,25 +37,12 @@ def instantiate_class(name: str, obj: Any): print(f"Class '{name}' requires the following parameters for instantiation:") for param in required_parameters: print(f"- {param.name}") + return None else: print(f"Class '{name}' does not require any parameters for instantiation.") # Instantiate the class instance = obj() - print(f"Instantiated: {name} -> {instance}") + return instance else: print(f"Class '{name}' does not have a constructor (__init__ method).") - - -def is_concrete(obj: Any): - return inspect.isclass(obj) and not inspect.isabstract(obj) - - -@pytest.mark.parametrize("class_name, obj", inspect.getmembers(TSC, is_concrete)) -def test_by_reflection(class_name, obj): - instantiate_class(class_name, obj) - - -@pytest.mark.parametrize("model", _models.get_defined_models()) -def test_repr_is_implemented(model): - print(model.__name__, type(model.__repr__).__name__) - assert type(model.__repr__).__name__ == "function" + return None diff --git a/test/test_custom_view.py b/test/test_custom_view.py index b2117358a..2a3932726 100644 --- a/test/test_custom_view.py +++ b/test/test_custom_view.py @@ -175,6 +175,7 @@ def test_publish_filepath(server: TSC.Server) -> None: cv._owner = TSC.UserItem() cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" with requests_mock.mock() as m: m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) @@ -191,6 +192,7 @@ def test_publish_file_str(server: TSC.Server) -> None: cv._owner = TSC.UserItem() cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" with requests_mock.mock() as m: m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) @@ -207,6 +209,7 @@ def test_publish_file_io(server: TSC.Server) -> None: cv._owner = TSC.UserItem() cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" data = io.BytesIO(CUSTOM_VIEW_DOWNLOAD.read_bytes()) with requests_mock.mock() as m: @@ -223,6 +226,7 @@ def test_publish_missing_owner_id(server: TSC.Server) -> None: cv = TSC.CustomViewItem(name="test") cv._owner = TSC.UserItem() cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" with requests_mock.mock() as m: m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) @@ -246,6 +250,7 @@ def test_large_publish(server: TSC.Server): cv._owner = TSC.UserItem() cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" with ExitStack() as stack: temp_dir = stack.enter_context(TemporaryDirectory())