Skip to content

Commit 2a73480

Browse files
authored
Merge pull request #875 from bodintsov/feature/share-cleanupgrade-2025-type-annotations
[ENG-7443] Feature/share cleanupgrade 2025 type annotations
2 parents d754725 + 45b79a1 commit 2a73480

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1199
-897
lines changed

.github/workflows/run_tests.yml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@ permissions:
99
checks: write # for coveralls
1010

1111
jobs:
12+
lint_and_type:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Install poetry
18+
run: pipx install poetry
19+
20+
- name: setup python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.13'
24+
cache: 'poetry'
25+
26+
- name: install despondencies
27+
run: poetry install --with dev
28+
29+
- name: flake it
30+
run: poetry run flake8 .
31+
32+
- name: type-check
33+
run: poetry run mypy trove
34+
1235
run_tests:
1336
strategy:
1437
fail-fast: false
@@ -60,9 +83,6 @@ jobs:
6083
- name: install despondencies
6184
run: poetry install --with dev
6285

63-
- name: flake it
64-
run: poetry run flake8 .
65-
6686
- name: run tests
6787
run: |
6888
poetry run coverage run -m pytest --create-db -x

mypy.ini

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
[mypy]
2+
python_version = 3.13
3+
plugins = mypy_django_plugin.main
4+
5+
# display options
6+
show_column_numbers = True
7+
pretty = True
8+
9+
# start with an ideal: enable strict type-checking, then loosen in module-specific config
10+
# see https://mypy.readthedocs.io/en/stable/existing_code.html#introduce-stricter-options
11+
strict = True
12+
## BEGIN possible loosenings from `strict`:
13+
# disallow_subclassing_any = False
14+
# warn_unused_configs = False
15+
# warn_redundant_casts = False
16+
# warn_unused_ignores = False
17+
# strict_equality = False
18+
# strict_concatenate = False
19+
# check_untyped_defs = False
20+
# disallow_untyped_decorators = False
21+
# disallow_any_generics = False
22+
# disallow_untyped_calls = False
23+
# disallow_incomplete_defs = False
24+
# disallow_untyped_defs = False
25+
# no_implicit_reexport = False
26+
# warn_return_any = False
27+
## END loosenings of `strict`
28+
29+
# prefer types that can be understood by reading code in only one place
30+
local_partial_types = True
31+
# avoid easily-avoidable dead code
32+
warn_unreachable = True
33+
# prefer direct imports
34+
implicit_reexport = False
35+
36+
# got untyped dependencies -- this is fine
37+
ignore_missing_imports = True
38+
disable_error_code = import-untyped,import-not-found
39+
40+
###
41+
# plugin config
42+
[mypy.plugins.django-stubs]
43+
django_settings_module = project.settings
44+
45+
###
46+
# module-specific config
47+
48+
## sharev2 code; largely unannotated
49+
[mypy-share.*,api.*,project.*,osf_oauth2_adapter.*,manage]
50+
# loosen strict:
51+
disallow_subclassing_any = False
52+
disallow_untyped_decorators = False
53+
disallow_any_generics = False
54+
disallow_untyped_calls = False
55+
disallow_incomplete_defs = False
56+
disallow_untyped_defs = False
57+
warn_return_any = False
58+
59+
## django migrations are whatever
60+
[mypy-*.migrations.*]
61+
strict = False
62+
#disable_error_code = var-annotated,import-untyped,import-not-found
63+
disallow_subclassing_any = False
64+
65+
## tests are looser
66+
[mypy-tests.*]
67+
disallow_untyped_defs = False
68+
69+
## trove code (trying to be) well-annotated (but still uses django...)
70+
[mypy-trove.*]
71+
disallow_subclassing_any = False
72+
disallow_untyped_decorators = False
73+
disallow_any_generics = False
74+
warn_return_any = False
75+
[mypy-trove.views.*]
76+
disallow_untyped_defs = False
77+
disallow_incomplete_defs = False

poetry.lock

Lines changed: 426 additions & 312 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ flake8 = "7.2.0"
6262
pytest-benchmark = "5.1.0"
6363
pytest = "8.3.5"
6464
pytest-django = "4.11.1"
65+
mypy = "1.16.1"
66+
django-stubs = "5.2.1"
6567

6668
###
6769
# other stuff

share/admin/celery.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ def lookups(self, request, model_admin):
3939
return sorted((x, x.title()) for x in states.ALL_STATES)
4040

4141
def queryset(self, request, queryset):
42-
if self.value():
43-
return queryset.filter(status=self.value().upper())
42+
_value = self.value()
43+
if _value:
44+
return queryset.filter(status=_value.upper())
4445
return queryset
4546

4647

@@ -99,15 +100,15 @@ def status_(self, obj):
99100
self.STATUS_COLORS.get(obj.status, 'black'),
100101
obj.status.title()
101102
)
102-
status_.short_description = 'Status'
103+
status_.short_description = 'Status' # type: ignore[attr-defined]
103104

104105
def meta_(self, obj):
105106
return pprint.pformat(obj.meta)
106-
meta_.short_description = 'Meta'
107+
meta_.short_description = 'Meta' # type: ignore[attr-defined]
107108

108109
def source_(self, obj):
109110
return obj.meta.get('source_config') or obj.meta.get('source')
110-
source_.short_description = 'Source'
111+
source_.short_description = 'Source' # type: ignore[attr-defined]
111112

112113
def retry(self, request, queryset):
113114
for task in queryset:
@@ -116,4 +117,4 @@ def retry(self, request, queryset):
116117
task.meta.get('kwargs', {}),
117118
task_id=str(task.task_id)
118119
)
119-
retry.short_description = 'Retry Tasks'
120+
retry.short_description = 'Retry Tasks' # type: ignore[attr-defined]

share/admin/util.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
from django.contrib.admin import SimpleListFilter
1+
from collections.abc import Callable, Sequence
2+
3+
from django.contrib.admin import SimpleListFilter, ModelAdmin
24
from django.core.paginator import Paginator
35
from django.db import connection, transaction, OperationalError
6+
from django.db.models import Model
47
from django.utils.functional import cached_property
58
from django.urls import reverse
69
from django.utils.html import format_html
@@ -46,27 +49,31 @@ def admin_link_html(linked_obj):
4649
return format_html('<a href="{}">{}</a>', url, repr(linked_obj))
4750

4851

49-
def linked_fk(field_name):
52+
def linked_fk[T: type[ModelAdmin]](field_name: str) -> Callable[[T], T]:
5053
"""Decorator that adds a link for a foreign key field
5154
"""
5255
def add_link(cls):
5356
def link(self, instance):
5457
linked_obj = getattr(instance, field_name)
5558
return admin_link_html(linked_obj)
5659
link_field = '{}_link'.format(field_name)
57-
link.short_description = field_name.replace('_', ' ')
60+
link.short_description = field_name.replace('_', ' ') # type: ignore[attr-defined]
5861
setattr(cls, link_field, link)
5962
append_to_cls_property(cls, 'readonly_fields', link_field)
6063
append_to_cls_property(cls, 'exclude', field_name)
6164
return cls
6265
return add_link
6366

6467

65-
def linked_many(field_name, order_by=None, select_related=None, defer=None):
66-
"""Decorator that adds links for a *-to-many field
67-
"""
68-
def add_links(cls):
69-
def links(self, instance):
68+
def linked_many[T: type[ModelAdmin]](
69+
field_name: str,
70+
order_by: Sequence[str] = (),
71+
select_related: Sequence[str] = (),
72+
defer: Sequence[str] = (),
73+
) -> Callable[[T], T]:
74+
"""Decorator that adds links for a *-to-many field"""
75+
def add_links(cls: T) -> T:
76+
def links(self, instance: Model) -> str:
7077
linked_qs = getattr(instance, field_name).all()
7178
if select_related:
7279
linked_qs = linked_qs.select_related(*select_related)
@@ -81,8 +88,8 @@ def links(self, instance):
8188
for obj in linked_qs
8289
))
8390
)
84-
links_field = '{}_links'.format(field_name)
85-
links.short_description = field_name.replace('_', ' ')
91+
links_field = f'{field_name}_links'
92+
links.short_description = field_name.replace('_', ' ') # type: ignore[attr-defined]
8693
setattr(cls, links_field, links)
8794
append_to_cls_property(cls, 'readonly_fields', links_field)
8895
append_to_cls_property(cls, 'exclude', field_name)

share/models/source_config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import annotations
12

23
from django.db import models
34

@@ -9,13 +10,13 @@
910
__all__ = ('SourceConfig',)
1011

1112

12-
class SourceConfigManager(models.Manager):
13+
class SourceConfigManager(models.Manager['SourceConfig']):
1314
use_in_migrations = True
1415

15-
def get_by_natural_key(self, key):
16+
def get_by_natural_key(self, key) -> SourceConfig:
1617
return self.get(label=key)
1718

18-
def get_or_create_push_config(self, user, transformer_key=None):
19+
def get_or_create_push_config(self, user, transformer_key=None) -> SourceConfig:
1920
assert isinstance(user, ShareUser)
2021
_config_label = '.'.join((
2122
user.username,

share/models/source_unique_identifier.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class JSONAPIMeta(BaseJSONAPIMeta):
1919
class Meta:
2020
unique_together = ('identifier', 'source_config')
2121

22-
def get_backcompat_sharev2_suid(self):
22+
def get_backcompat_sharev2_suid(self) -> 'SourceUniqueIdentifier':
2323
'''get an equivalent "v2_push" suid for this suid
2424
2525
for filling the legacy suid-based sharev2 index with consistent doc ids

share/oaipmh/util.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import datetime
2+
from typing import Any
3+
14
from lxml import etree
25
from primitive_metadata import primitive_rdf
36

47
from share.util.fromisoformat import fromisoformat
58
from trove.vocab.namespaces import OAI, OAI_DC
69

710

8-
def format_datetime(dt):
11+
def format_datetime(dt: datetime.datetime | primitive_rdf.Literal | str) -> str:
912
"""OAI-PMH has specific time format requirements -- comply.
1013
"""
1114
if isinstance(dt, primitive_rdf.Literal):
@@ -25,15 +28,15 @@ def format_datetime(dt):
2528
}
2629

2730

28-
def ns(namespace_prefix, tag_name):
31+
def ns(namespace_prefix: str, tag_name: str) -> str:
2932
"""format XML tag/attribute name with full namespace URI
3033
3134
see https://lxml.de/tutorial.html#namespaces
3235
"""
3336
return f'{{{XML_NAMESPACES[namespace_prefix]}}}{tag_name}'
3437

3538

36-
def nsmap(*namespace_prefixes, default=None):
39+
def nsmap(*namespace_prefixes: str, default: str | None = None) -> dict[str | None, str]:
3740
"""build a namespace map suitable for lxml
3841
3942
see https://lxml.de/tutorial.html#namespaces
@@ -49,7 +52,7 @@ def nsmap(*namespace_prefixes, default=None):
4952

5053

5154
# wrapper for lxml.etree.SubElement, adds `text` kwarg for convenience
52-
def SubEl(parent, tag_name, text=None, **kwargs):
55+
def SubEl(parent: etree.Element, tag_name: str, text: str | None = None, **kwargs: Any) -> etree.SubElement:
5356
element = etree.SubElement(parent, tag_name, **kwargs)
5457
if isinstance(text, primitive_rdf.Literal):
5558
_language_tag = text.language

share/search/index_messenger.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from share.search.messages import MessagesChunk, MessageType
1414
from share.search import index_strategy
15-
15+
from trove.models import Indexcard
1616

1717
logger = logging.getLogger(__name__)
1818

@@ -25,15 +25,15 @@ class IndexMessenger:
2525
'max_retries': 30, # give up after 30 tries.
2626
}
2727

28-
def __init__(self, *, celery_app=None, index_strategys=None):
28+
def __init__(self, *, celery_app=None, index_strategys=None) -> None:
2929
self.celery_app = (
3030
celery.current_app
3131
if celery_app is None
3232
else celery_app
3333
)
3434
self.index_strategys = index_strategys or tuple(index_strategy.each_strategy())
3535

36-
def notify_indexcard_update(self, indexcards, *, urgent=False):
36+
def notify_indexcard_update(self, indexcards: list[Indexcard], *, urgent=False) -> None:
3737
self.send_messages_chunk(
3838
MessagesChunk(
3939
MessageType.UPDATE_INDEXCARD,
@@ -53,7 +53,7 @@ def notify_indexcard_update(self, indexcards, *, urgent=False):
5353
urgent=urgent,
5454
)
5555

56-
def notify_suid_update(self, suid_ids, *, urgent=False):
56+
def notify_suid_update(self, suid_ids, *, urgent=False) -> None:
5757
self.send_messages_chunk(
5858
MessagesChunk(MessageType.INDEX_SUID, suid_ids),
5959
urgent=urgent,

0 commit comments

Comments
 (0)