diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 54466a09..2ffb4b10 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,10 +9,10 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 - name: Install build dependency run: pip install build diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2ac3e2af..4f28b5fe 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -40,7 +40,7 @@ jobs: - name: Check for missing kwargs run: python scripts/find_missing_kwargs.py - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v6 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.readthedocs.yml b/.readthedocs.yml index 151e7cc1..22e04999 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,12 +5,15 @@ # Required version: 2 +build: + os: ubuntu-lts-latest + tools: + python: "3.13" + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py python: - version: 3.7 install: - requirements: dev_requirements.txt - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bedc43e..15827094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ## [Unreleased] +## [3.6.0] - 2026-04-16 + +### General + +- Added support for Python 3.14 +- Dropped support for Python 3.9 + +### Bugfixes + +- Fixed unexpected behavior from the `obj_or_str` utility function that prevented `Feature`-related functions from accepting `str` arguments. + +### Backstage + +- Updated minimum `black` version to avoid CVE-2026-32274 +- Updated various GitHub Actions +- Update readthedocs config to include build info + ## [3.5.0] - 2026-03-12 ### General @@ -667,7 +684,8 @@ Huge thanks to [@liblit](https://github.com/liblit) for lots of issues, suggesti - Fixed some incorrectly defined parameters - Fixed an issue where tests would fail due to an improperly configured requires block -[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v3.5.0...develop +[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v3.6.0...develop +[3.6.0]: https://github.com/ucfopen/canvasapi/compare/v3.5.0...v3.6.0 [3.5.0]: https://github.com/ucfopen/canvasapi/compare/v3.4.0...v3.5.0 [3.4.0]: https://github.com/ucfopen/canvasapi/compare/v3.3.0...v3.4.0 [3.3.0]: https://github.com/ucfopen/canvasapi/compare/v3.2.0...v3.3.0 diff --git a/canvasapi/__init__.py b/canvasapi/__init__.py index 46277f79..f054979e 100644 --- a/canvasapi/__init__.py +++ b/canvasapi/__init__.py @@ -4,4 +4,4 @@ __all__ = ["Canvas"] -__version__ = "3.5.0" +__version__ = "3.6.0" diff --git a/canvasapi/util.py b/canvasapi/util.py index 21c2a02b..a4cc08ed 100644 --- a/canvasapi/util.py +++ b/canvasapi/util.py @@ -133,37 +133,34 @@ def obj_or_id(parameter, param_name, object_types): raise TypeError(message) -def obj_or_str(obj, attr, object_types): +def obj_or_str(parameter, param_name, object_types): """ - Accepts an object. If the object has the attribute, return the + Accepts either an object or a string. If it is a string, return it directly. + If it is an object and the object is of correct type, return the object's corresponding string. Otherwise, throw an exception. - :param obj: object from which to retrieve attribute - :type obj: object - :param attr: name of the attribute to retrieve - :type attr: str + :param parameter: object from which to retrieve attribute + :type parameter: str or object + :param param_name: name of the attribute to retrieve + :type param_name: str :param object_types: tuple containing the types of the object being passed in :type object_types: tuple :rtype: str """ - try: - return str(getattr(obj, attr)) - except (AttributeError, TypeError): - if not isinstance(attr, str): - raise TypeError( - "Atttibute parameter {} must be of type string".format(attr) - ) - for obj_type in object_types: - if isinstance(obj, obj_type): - try: - return str(getattr(obj, attr)) - except AttributeError: - raise AttributeError("{} object does not have {} attribute").format( - obj, attr - ) + if isinstance(parameter, str): + return parameter - obj_type_list = ",".join([obj_type.__name__ for obj_type in object_types]) - raise TypeError("Parameter {} must be of type {}.".format(obj, obj_type_list)) + for obj_type in object_types: + if isinstance(parameter, obj_type): + try: + return str(getattr(parameter, param_name)) + except AttributeError: + raise AttributeError("{} object does not have {} attribute").format( + parameter, param_name + ) + + obj_type_list = ",".join([obj_type.__name__ for obj_type in object_types]) + raise TypeError("Parameter {} must be of type {}.".format(parameter, obj_type_list)) def get_institution_url(base_url): diff --git a/setup.py b/setup.py index d0cedd3e..c700948c 100644 --- a/setup.py +++ b/setup.py @@ -39,11 +39,11 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries", ], ) diff --git a/tests/test_util.py b/tests/test_util.py index cd85648b..211ed597 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -431,6 +431,12 @@ def test_obj_or_id_nonuser_self(self, m): obj_or_id("self", "user_id", (CourseNickname,)) # obj_or_str() + def test_obj_or_str_str(self, m): + name = obj_or_str("test", "name", (User,)) + + self.assertIsInstance(name, str) + self.assertEqual(name, "test") + def test_obj_or_str_obj_attr(self, m): register_uris({"user": ["get_by_id"]}, m) @@ -467,8 +473,12 @@ def test_obj_or_str_invalid_attr_parameter(self, m): obj_or_str(user, user, (User,)) def test_obj_or_str_invalid_obj_type(self, m): + register_uris({"course": ["get_by_id"]}, m) + + course = self.canvas.get_course(1) + with self.assertRaises(TypeError): - obj_or_str("user", "name", (User,)) + obj_or_str(course, "name", (User,)) # get_institution_url() def test_get_institution_url(self, m): diff --git a/tests_requirements.txt b/tests_requirements.txt index d83fdc09..dca1f670 100644 --- a/tests_requirements.txt +++ b/tests_requirements.txt @@ -1,6 +1,6 @@ -r requirements.txt -black~=25.1.0 +black~=26.3.1 coverage flake8 isort