Skip to content

Commit 05a3b8d

Browse files
authored
add type hints (#1343)
* add type hints: part 1 Converting old docstring-based type hints to PEP 484 type hints. This commit includes all of cms/ except for cms/server/, and all of cmscommon/. * python 3.11 compatibility (use typing.TypeVar instead of new syntax) * add type hints part 2 Added type hints to cms/server and fixed some previous type hint mistakes. * type hints for cmsranking * more type hint fixes guard all tornado imports behind `if typing.TYPE_CHECKING`. i think this allows the "tornado4" workaround to still work, though i don't know if it's relevant anymore. * type hints for cmscontrib (and prerequisites.py) * type hints for cmstestsuite * make PriorityQueue and TriggeredService generic * Update Python version requirement to 3.11 * Run `black` on modified files * update CONTRIBUTING.md * address review comments * Some better type hints on FileCacher / LargeObject Use typing.IO[bytes] instead of BinaryIO, because some stdlib types are apparently only compatible with the former.
1 parent 7c9fec8 commit 05a3b8d

File tree

175 files changed

+4303
-3201
lines changed

Some content is hidden

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

175 files changed

+4303
-3201
lines changed

.github/CONTRIBUTING.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,36 @@ For Python code, we generally follow [PEP 8](https://www.python.org/dev/peps/pep
3131
We get around Python flexible type system in several ways:
3232
* we try to avoid "magic" (e.g., generating or changing classes on the fly);
3333
* we are fairly verbose with naming, trying to help the reader with following the types;
34-
* we follow our type annotation system for method and function docstrings
35-
(planning to switch to [PEP 484](https://www.python.org/dev/peps/pep-0484/));
36-
see later for the format.
34+
* We use [PEP 484](https://www.python.org/dev/peps/pep-0484/) type annotations.
3735

38-
We support Python 3 only, requiring at least version 3.9.
36+
We support Python 3 only. See the Installation documentation for the current
37+
minimum supported Python version.
3938

40-
# Docstring type annotation format
39+
# Docstring format
4140

42-
We use a custom format for type annotation in method and function docstrings. Here's an example taken from the code:
41+
Our docstring format is simple, if a little nonstandard.
42+
Here's an example taken from the code:
4343

4444
```
4545
class Cls(object):
4646
[...]
47-
def example(self, a, b, c=None):
47+
def example(
48+
self, a: int, b: list[dict[str, int]], c: Submission | None = None
49+
) -> tuple[int, str]:
4850
"""Perform an example action, described here in one line.
4951
5052
This is a longer description of what the method does and can
5153
occupy more than one line, each shorter than 80 characters.
5254
53-
a (int): a is a required integer.
54-
b ([{str: int}]): b is a list of dictionaries mapping strings to
55-
integers, and note how the docstring wraps with indent.
56-
c (Submission|None): c is either a Submission (not required to
57-
fully specify, but it could be helpful for symbols that are
58-
not imported) or None.
55+
a: a is a required integer.
56+
b: b is a list of dictionaries mapping strings to integers, and
57+
note how the docstring wraps with indent.
58+
c: c is either a Submission (not required to fully specify, but
59+
it could be helpful for symbols that are not imported) or
60+
None.
5961
60-
return ((int, str)): this method returns a tuple containing an
61-
integer and a string.
62+
return: this method returns a tuple containing an integer and a
63+
string.
6264
6365
raise (ValueError): if a is negative.
6466
raise (LookupError): if we could not find something.

cms/conf.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,28 @@
2626
import logging
2727
import os
2828
import sys
29-
from collections import namedtuple
29+
import typing
3030

31-
from .log import set_detailed_logs
31+
from cms.log import set_detailed_logs
3232

3333

3434
logger = logging.getLogger(__name__)
3535

3636

37-
class Address(namedtuple("Address", "ip port")):
37+
class Address(typing.NamedTuple):
38+
ip: str
39+
port: int
3840
def __repr__(self):
3941
return "%s:%d" % (self.ip, self.port)
4042

4143

42-
class ServiceCoord(namedtuple("ServiceCoord", "name shard")):
44+
class ServiceCoord(typing.NamedTuple):
4345
"""A compact representation for the name and the shard number of a
4446
service (thus identifying it).
4547
4648
"""
49+
name: str
50+
shard: int
4751
def __repr__(self):
4852
return "%s,%d" % (self.name, self.shard)
4953

@@ -67,8 +71,8 @@ class AsyncConfig:
6771
anyway not constantly.
6872
6973
"""
70-
core_services = {}
71-
other_services = {}
74+
core_services: dict[ServiceCoord, Address] = {}
75+
other_services: dict[ServiceCoord, Address] = {}
7276

7377

7478
async_config = AsyncConfig()
@@ -184,7 +188,7 @@ def __init__(self):
184188
# change the log configuration.
185189
set_detailed_logs(self.stream_log_detailed)
186190

187-
def _load(self, paths):
191+
def _load(self, paths: list[str]):
188192
"""Try to load the config files one at a time, until one loads
189193
correctly.
190194
@@ -196,7 +200,7 @@ def _load(self, paths):
196200
logging.warning("No configuration file found: "
197201
"falling back to default values.")
198202

199-
def _load_unique(self, path):
203+
def _load_unique(self, path: str):
200204
"""Populate the Config class with everything that sits inside
201205
the JSON file path (usually something like /etc/cms.conf). The
202206
only pieces of data treated differently are the elements of
@@ -206,7 +210,7 @@ def _load_unique(self, path):
206210
Services whose name begins with an underscore are ignored, so
207211
they can be commented out in the configuration file.
208212
209-
path (string): the path of the JSON config file.
213+
path: the path of the JSON config file.
210214
211215
"""
212216
# Load config file.

cms/db/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,14 @@
119119
# The following is a method of Dataset that cannot be put in the right
120120
# file because of circular dependencies.
121121

122-
def get_submission_results_for_dataset(self, dataset):
122+
def get_submission_results_for_dataset(self, dataset) -> list[SubmissionResult]:
123123
"""Return a list of all submission results against the specified
124124
dataset.
125125
126126
Also preloads the executable and evaluation objects relative to
127127
the submission results.
128128
129-
returns ([SubmissionResult]): list of submission results.
129+
returns: list of submission results.
130130
131131
"""
132132
# We issue this query manually to optimize it: we load all

cms/db/admin.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
from sqlalchemy.schema import Column
2424
from sqlalchemy.types import Boolean, Integer, Unicode
2525

26-
from . import Codename, Base
26+
from .types import Codename
27+
from .base import Base
2728

2829

2930
class Admin(Base):
@@ -38,46 +39,46 @@ class Admin(Base):
3839
__tablename__ = 'admins'
3940

4041
# Auto increment primary key.
41-
id = Column(
42+
id: int = Column(
4243
Integer,
4344
primary_key=True)
4445

4546
# Real name (human readable) of the user.
46-
name = Column(
47+
name: str = Column(
4748
Unicode,
4849
nullable=False)
4950

5051
# Username used to log in in AWS.
51-
username = Column(
52+
username: str = Column(
5253
Codename,
5354
nullable=False,
5455
unique=True)
5556

5657
# String used to authenticate the user, in the format
5758
# <authentication type>:<authentication_string>
58-
authentication = Column(
59+
authentication: str = Column(
5960
Unicode,
6061
nullable=False)
6162

6263
# Whether the account is enabled. Disabled accounts have their
6364
# info kept in the database, but for all other purposes it is like
6465
# they did not exist.
65-
enabled = Column(
66+
enabled: bool = Column(
6667
Boolean,
6768
nullable=False,
6869
default=True)
6970

7071
# All-access bit. If this is set, the admin can do any operation
7172
# in AWS, regardless of the value of the other access bits.
72-
permission_all = Column(
73+
permission_all: bool = Column(
7374
Boolean,
7475
nullable=False,
7576
default=False)
7677

7778
# Messaging-access bit. If this is set, the admin can communicate
7879
# with the contestants via announcement, private messages and
7980
# questions.
80-
permission_messaging = Column(
81+
permission_messaging: bool = Column(
8182
Boolean,
8283
nullable=False,
8384
default=False)

cms/db/base.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import ipaddress
2323
from datetime import datetime, timedelta
24+
import typing
2425

2526
from sqlalchemy.dialects.postgresql import ARRAY, CIDR, JSONB, OID
2627
from sqlalchemy.ext.declarative import as_declarative
@@ -32,6 +33,8 @@
3233
Boolean, Integer, Float, String, Unicode, Enum, DateTime, Interval, \
3334
BigInteger
3435

36+
from cms.db.session import Session
37+
3538
from . import engine, metadata, CastingArray, Codename, Filename, \
3639
FilenameSchema, FilenameSchemaArray, Digest
3740

@@ -59,7 +62,8 @@
5962
}
6063

6164

62-
@as_declarative(bind=engine, metadata=metadata, constructor=None)
65+
# this has an @as_declarative, but to ease type checking it's applied manually
66+
# after the class definition, only when not type-checking (i.e. at runtime).
6367
class Base:
6468
"""Base class for all classes managed by SQLAlchemy. Extending the
6569
base class given by SQLAlchemy.
@@ -70,7 +74,7 @@ def sa_mapper(self):
7074
return object_mapper(self)
7175

7276
@property
73-
def sa_session(self):
77+
def sa_session(self) -> Session:
7478
return object_session(self)
7579

7680
@property
@@ -187,20 +191,20 @@ def __init__(self, *args, **kwargs):
187191
raise
188192

189193
@classmethod
190-
def get_from_id(cls, id_, session):
194+
def get_from_id(cls, id_: tuple | int | str, session: Session) -> typing.Self | None:
191195
"""Retrieve an object from the database by its ID.
192196
193197
Use the given session to fetch the object of this class with
194198
the given ID, and return it. If it doesn't exist return None.
195199
196-
cls (type): the class to which the method is attached.
197-
id_ (tuple, int or string): the ID of the object we want; in
200+
cls: the class to which the method is attached.
201+
id_: the ID of the object we want; in
198202
general it will be a tuple (one int for each of the columns
199203
that make up the primary key) but if there's only one then
200204
a single int (even encoded as unicode or bytes) will work.
201-
session (Session): the session to query.
205+
session: the session to query.
202206
203-
return (Base|None): the desired object, or None if not found.
207+
return: the desired object, or None if not found.
204208
205209
"""
206210
try:
@@ -213,26 +217,26 @@ def get_from_id(cls, id_, session):
213217
except ObjectDeletedError:
214218
return None
215219

216-
def clone(self):
220+
def clone(self) -> typing.Self:
217221
"""Copy all the column properties into a new object
218222
219223
Create a new object of this same type and set the values of all
220224
its column properties to the ones of this "old" object. Leave
221225
the relationship properties unset.
222226
223-
return (object): a clone of this object
227+
return: a clone of this object
224228
225229
"""
226230
cls = type(self)
227231
args = list(getattr(self, prp.key) for prp in self._col_props)
228232
return cls(*args)
229233

230-
def get_attrs(self):
234+
def get_attrs(self) -> dict[str, object]:
231235
"""Return self.__dict__.
232236
233237
Limited to SQLAlchemy column properties.
234238
235-
return ({string: object}): the properties of this object.
239+
return: the properties of this object.
236240
237241
"""
238242
attrs = dict()
@@ -241,14 +245,14 @@ def get_attrs(self):
241245
attrs[prp.key] = getattr(self, prp.key)
242246
return attrs
243247

244-
def set_attrs(self, attrs, fill_with_defaults=False):
248+
def set_attrs(self, attrs: typing.Mapping[str, object], fill_with_defaults: bool = False):
245249
"""Do self.__dict__.update(attrs) with validation.
246250
247251
Limited to SQLAlchemy column and relationship properties.
248252
249-
attrs ({string: object}): the new properties we want to set on
253+
attrs: the new properties we want to set on
250254
this object.
251-
fill_with_defaults (bool): whether to explicitly reset the
255+
fill_with_defaults: whether to explicitly reset the
252256
attributes that were not provided in attrs to their default
253257
value.
254258
@@ -318,3 +322,9 @@ def set_attrs(self, attrs, fill_with_defaults=False):
318322
"set_attrs() got an unexpected keyword argument '%s'" %
319323
attrs.popitem()[0])
320324

325+
# don't apply the decorator when type checking, as as_declarative doesn't have
326+
# enough type hints for pyright to consider it valid. This means that pyright
327+
# doesn't consider Base to be a valid base class, and thus all derived classes
328+
# will be missing the methods from Base.
329+
if not typing.TYPE_CHECKING:
330+
Base = as_declarative(bind=engine, metadata=metadata, constructor=None)(Base)

0 commit comments

Comments
 (0)