Skip to content

Commit 3bc000e

Browse files
committed
Added type-checking.
1 parent 7555a1f commit 3bc000e

File tree

16 files changed

+513
-265
lines changed

16 files changed

+513
-265
lines changed

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
'members': True, # Include module/class members.
5454
'member-order': 'bysource', # Order members as in source file.
5555
}
56+
autodoc_typehints = 'none' # Rendering type hints doesn't work.
5657
autosummary_generate = False # Stub files are created by hand.
5758
add_module_names = False # Drop module prefix from signatures.
5859

mph/client.py

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Provides the wrapper for a Comsol client instance."""
22

3+
from __future__ import annotations
4+
35
from . import discovery
46
from .model import Model
57
from .config import option
@@ -11,8 +13,9 @@
1113
from pathlib import Path
1214
from logging import getLogger
1315

16+
from typing import overload, Iterator
17+
from jpype import JClass
1418

15-
log = getLogger(__package__)
1619

1720
# The following look-up table is used by the `modules()` method. It is
1821
# based on the table on page 41 of Comsol 6.0's Programming Reference
@@ -72,6 +75,8 @@
7275
'WAVEOPTICS': 'Wave Optics',
7376
}
7477

78+
log = getLogger(__package__)
79+
7580

7681
class Client:
7782
"""
@@ -121,11 +126,31 @@ class Client:
121126
/com/comsol/model/util/ModelUtil.html
122127
"""
123128

129+
version: str
130+
"""Comsol version (e.g., `'6.0'`) the client is running on."""
131+
132+
standalone: bool
133+
"""Whether this is a stand-alone client or connected to a server."""
134+
135+
port: int | None
136+
"""Port number on which the client has connected to the server."""
137+
138+
host: str | None
139+
"""Host name or IP address of the server the client is connected to."""
140+
141+
java: JClass
142+
"""Java model object that this class instance is wrapped around."""
143+
124144
############
125145
# Internal #
126146
############
127147

128-
def __init__(self, cores=None, version=None, port=None, host='localhost'):
148+
def __init__(self,
149+
cores: int = None,
150+
version: str = None,
151+
port: int = None,
152+
host: str = 'localhost',
153+
):
129154

130155
# Make sure this is the one and only client.
131156
if jpype.isJVMStarted():
@@ -206,27 +231,18 @@ def __init__(self, cores=None, version=None, port=None, host='localhost'):
206231
# Log that we're done so the start-up time may be inspected.
207232
log.info('Stand-alone client initialized.')
208233

209-
# Save and document instance attributes.
210-
# It seems to be necessary to document the instance attributes here
211-
# towards the end of the method. If done earlier, Sphinx would not
212-
# render them in source-code order, even though that's what we
213-
# request in the configuration. This might be a bug in Sphinx.
214-
self.version = backend['name']
215-
"""Comsol version (e.g., `'6.0'`) the client is running on."""
234+
# Save instance attributes.
235+
self.version = backend['name']
216236
self.standalone = standalone
217-
"""Whether this is a stand-alone client or connected to a server."""
218-
self.port = None
219-
"""Port number on which the client has connected to the server."""
220-
self.host = None
221-
"""Host name or IP address of the server the client is connected to."""
222-
self.java = java
223-
"""Java model object that this class instance is wrapped around."""
237+
self.port = None
238+
self.host = None
239+
self.java = java
224240

225241
# Try to connect to server if not a stand-alone client.
226-
if not standalone and host:
242+
if not standalone and host and port is not None:
227243
self.connect(port, host)
228244

229-
def __repr__(self):
245+
def __repr__(self) -> str:
230246
if self.standalone:
231247
connection = 'stand-alone'
232248
elif self.port:
@@ -235,17 +251,17 @@ def __repr__(self):
235251
connection = 'disconnected'
236252
return f'{self.__class__.__name__}({connection})'
237253

238-
def __contains__(self, item):
254+
def __contains__(self, item: str | Model) -> bool:
239255
if isinstance(item, str) and item in self.names():
240256
return True
241257
if isinstance(item, Model) and item in self.models():
242258
return True
243259
return False
244260

245-
def __iter__(self):
261+
def __iter__(self) -> Iterator[Model]:
246262
yield from self.models()
247263

248-
def __truediv__(self, name):
264+
def __truediv__(self, name: str) -> Model:
249265
if isinstance(name, str):
250266
for model in self:
251267
if name == model.name():
@@ -262,25 +278,25 @@ def __truediv__(self, name):
262278
##############
263279

264280
@property
265-
def cores(self):
281+
def cores(self) -> int:
266282
"""Number of processor cores (threads) the Comsol session is using."""
267283
cores = self.java.getPreference('cluster.processor.numberofprocessors')
268284
cores = int(str(cores))
269285
return cores
270286

271-
def models(self):
287+
def models(self) -> list[Model]:
272288
"""Returns all models currently held in memory."""
273289
return [Model(self.java.model(tag)) for tag in self.java.tags()]
274290

275-
def names(self):
291+
def names(self) -> list[str]:
276292
"""Returns the names of all loaded models."""
277293
return [model.name() for model in self.models()]
278294

279-
def files(self):
295+
def files(self) -> list[Path]:
280296
"""Returns the file-system paths of all loaded models."""
281297
return [model.file() for model in self.models()]
282298

283-
def modules(self):
299+
def modules(self) -> list[str]:
284300
"""Returns the names of available licensed modules/products."""
285301
names = []
286302
for (key, value) in modules.items():
@@ -295,7 +311,7 @@ def modules(self):
295311
# Interaction #
296312
###############
297313

298-
def load(self, file):
314+
def load(self, file: Path | str) -> Model:
299315
"""Loads a model from the given `file` and returns it."""
300316
file = Path(file).resolve()
301317
if self.caching() and file in self.files():
@@ -307,6 +323,10 @@ def load(self, file):
307323
log.info('Finished loading model.')
308324
return model
309325

326+
@overload
327+
def caching(self, state: None) -> bool: ...
328+
@overload
329+
def caching(self, state: bool): ...
310330
def caching(self, state=None):
311331
"""
312332
Enables or disables caching of previously loaded models.
@@ -329,7 +349,7 @@ def caching(self, state=None):
329349
log.error(error)
330350
raise ValueError(error)
331351

332-
def create(self, name=None):
352+
def create(self, name: str = None) -> Model:
333353
"""
334354
Creates a new model and returns it as a [`Model`](#Model) instance.
335355
@@ -345,7 +365,7 @@ def create(self, name=None):
345365
log.debug(f'Created model "{name}" with tag "{java.tag()}".')
346366
return model
347367

348-
def remove(self, model):
368+
def remove(self, model: str | Model):
349369
"""Removes the given [`model`](#Model) from memory."""
350370
if isinstance(model, str):
351371
if model not in self.names():
@@ -382,7 +402,7 @@ def clear(self):
382402
# Remote #
383403
##########
384404

385-
def connect(self, port, host='localhost'):
405+
def connect(self, port: int, host: str = 'localhost'):
386406
"""
387407
Connects the client to a server.
388408

mph/config.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
"""Manages configuration options."""
22

3+
from __future__ import annotations
4+
35
import configparser
46
import os
57
import platform
68
from pathlib import Path
79
from logging import getLogger
810

11+
from typing import overload, TypedDict
12+
13+
14+
class Options(TypedDict):
15+
session: str
16+
caching: bool
17+
classkit: bool
918

10-
system = platform.system()
11-
log = getLogger(__package__)
1219

1320
options = {
1421
'session': 'platform-dependent',
@@ -17,11 +24,20 @@
1724
}
1825
"""Default values for configuration options."""
1926

27+
system = platform.system()
28+
log = getLogger(__package__)
29+
2030

2131
##########
2232
# Access #
2333
##########
2434

35+
@overload
36+
def option(name: None, value: None) -> Options: ...
37+
@overload
38+
def option(name: str, value: None) -> str | bool | int | float: ...
39+
@overload
40+
def option(name: str, value: str | bool | int | float): ...
2541
def option(name=None, value=None):
2642
"""
2743
Sets or returns the value of a configuration option.
@@ -46,7 +62,7 @@ def option(name=None, value=None):
4662
# Storage #
4763
###########
4864

49-
def location():
65+
def location() -> Path:
5066
"""
5167
Returns the default location of the configuration file.
5268
@@ -65,7 +81,7 @@ def location():
6581
return Path.home()/'MPh'
6682

6783

68-
def load(file=None):
84+
def load(file: Path | str = None):
6985
"""
7086
Loads the configuration from the given `.ini` file.
7187
@@ -104,7 +120,7 @@ def load(file=None):
104120
options[key] = parser[section][key]
105121

106122

107-
def save(file=None):
123+
def save(file: Path | str = None):
108124
"""
109125
Saves the configuration in the given `.ini` file.
110126

mph/discovery.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,32 @@
2121
found in an earlier step will be ignored, regardless of install location.
2222
"""
2323

24+
from __future__ import annotations
25+
2426
import platform
2527
import subprocess
2628
import re
2729
from pathlib import Path
2830
from functools import lru_cache
2931
from logging import getLogger
3032

33+
from typing import TypedDict
34+
35+
36+
class Backend(TypedDict):
37+
name: str
38+
major: int
39+
minor: int
40+
patch: int
41+
build: int
42+
root: Path
43+
jvm: Path
44+
server: list[Path | str]
45+
3146

32-
log = getLogger(__package__)
3347
system = platform.system()
48+
log = getLogger(__package__)
49+
3450
architectures = {
3551
'Windows': ['win64'],
3652
'Linux': ['glnxa64'],
@@ -42,7 +58,7 @@
4258
# Version information #
4359
#######################
4460

45-
def parse(version):
61+
def parse(version: str) -> tuple[str, int, int, int, int]:
4662
"""
4763
Parses version information as returned by Comsol executable.
4864
@@ -86,7 +102,7 @@ def parse(version):
86102
# Back-end discovery #
87103
######################
88104

89-
def search_registry():
105+
def search_registry() -> list[Path]:
90106
"""Returns Comsol executables found in the Windows Registry."""
91107

92108
log.debug('Searching Windows Registry for Comsol executables.')
@@ -153,7 +169,7 @@ def search_registry():
153169
return executables
154170

155171

156-
def search_disk():
172+
def search_disk() -> list[Path]:
157173
"""Returns Comsol executables found on the file system."""
158174

159175
log.debug('Searching file system for Comsol executables.')
@@ -201,7 +217,7 @@ def search_disk():
201217
return executables
202218

203219

204-
def lookup_comsol():
220+
def lookup_comsol() -> Path | None:
205221
"""Returns Comsol executable if found on the system's search path."""
206222

207223
log.debug('Looking for Comsol executable on system search path.')
@@ -238,7 +254,7 @@ def lookup_comsol():
238254

239255

240256
@lru_cache(maxsize=1)
241-
def find_backends():
257+
def find_backends() -> list[Backend]:
242258
"""Returns the list of available Comsol back-ends."""
243259

244260
log.debug('Searching system for available Comsol back-ends.')
@@ -296,12 +312,13 @@ def find_backends():
296312
continue
297313

298314
# On Windows, check that server executable exists in same folder.
315+
server: list[Path | str]
299316
if system == 'Windows':
300-
server = ini.parent/'comsolmphserver.exe'
301-
if not server.exists():
317+
file = ini.parent/'comsolmphserver.exe'
318+
if not file.exists():
302319
log.debug(f'No server executable alongside "{ini.name}".')
303320
continue
304-
server = [server]
321+
server = [file]
305322
else:
306323
server = [comsol, 'mphserver']
307324

@@ -340,6 +357,7 @@ def find_backends():
340357
continue
341358

342359
# Get version information from Comsol server.
360+
command: list[Path | str]
343361
command = server + ['--version']
344362
command[0] = str(command[0]) # Needed to support Python 3.6 and 3.7.
345363
try:
@@ -351,7 +369,10 @@ def find_backends():
351369
# `universal_newlines` instead of `text` to support Python 3.6.
352370
if system == 'Windows':
353371
arguments['creationflags'] = 0x08000000
354-
process = subprocess.run(command, **arguments)
372+
process = subprocess.run(
373+
command,
374+
**arguments, # pyright: ignore[reportArgumentType]
375+
)
355376
except subprocess.CalledProcessError:
356377
log.debug('Querying version information failed.')
357378
continue
@@ -395,7 +416,7 @@ def find_backends():
395416
# Back-end selection #
396417
######################
397418

398-
def backend(version=None):
419+
def backend(version: str = None) -> Backend:
399420
"""
400421
Returns information about the Comsol back-end.
401422

0 commit comments

Comments
 (0)