Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
7 changes: 5 additions & 2 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion canvasapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

__all__ = ["Canvas"]

__version__ = "3.5.0"
__version__ = "3.6.0"
43 changes: 20 additions & 23 deletions canvasapi/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
12 changes: 11 additions & 1 deletion tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-r requirements.txt

black~=25.1.0
black~=26.3.1
coverage
flake8
isort
Expand Down