Skip to content

Commit

Permalink
Merge pull request #213 from seismic-anisotropy/fix-pytest-logging
Browse files Browse the repository at this point in the history
Fix logging/pytest setup and make mock objects subclasses of `DefaultParams`
  • Loading branch information
Patol75 authored Aug 14, 2024
2 parents ac839e4 + c057ae5 commit 6ff7d88
Show file tree
Hide file tree
Showing 16 changed files with 467 additions and 378 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to toggle verbose doctest output using `pytest -vv`

### Changed
- `mock` objects to be subclasses of `core.DefaultParams` with new symbol names
- Symbol name of `pydrex.logger.handler_level` to `pydrex.io.log_cli_level`
- Call signature for steady flow 2D box visualisation function
- Symbol names for default stiffness tensors (now members of
`minerals.StiffnessTensors` — use your own preferred stiffness tensors by
Expand All @@ -34,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Handling of enstatite in Voigt averaging
- Handling of optional keyword args in some visualisation functions

### Removed
- Access to `io` symbols in global `pydrex` namespace (use `pydrex.io` instead)

## [0.0.1] - 2024-04-24

Expand Down
17 changes: 14 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.SHELLFLAGS += -u
.ONESHELL:
MAKEFLAGS += --no-builtin-rules
VERSION = $(shell git describe)
VERSION = $(shell python -m setuptools_scm -f plain)

# NOTE: Keep this at the top! Bare `make` should just build the sdist + bdist.
dist:
Expand All @@ -14,15 +14,26 @@ test:
mkdir out
pytest -v --outdir=out

# WARNING: --math fetches .js code from a CDN, be careful where it comes from:
# https://github.com/mitmproxy/pdoc/security/advisories/GHSA-5vgj-ggm4-fg62
html:
2>pdoc.log pdoc -t docs/template -o html pydrex !pydrex.mesh !pydrex.distributed tests \
--favicon "https://raw.githubusercontent.com/seismic-anisotropy/PyDRex/main/docs/assets/favicon32.png" \
--footer-text "PyDRex $(VERSION)"
--footer-text "PyDRex $(VERSION)" \
--math

# WARNING: --math fetches .js code from a CDN, be careful where it comes from:
# https://github.com/mitmproxy/pdoc/security/advisories/GHSA-5vgj-ggm4-fg62
live_docs:
pdoc -t docs/template pydrex !pydrex.mesh !pydrex.distributed tests \
--favicon "https://raw.githubusercontent.com/seismic-anisotropy/PyDRex/main/docs/assets/favicon32.png" \
--footer-text "PyDRex $(VERSION)" \
--math

clean:
rm -rf dist
rm -rf out
rm -rf html
rm -rf pdoc.log

.PHONY: release test clean
.PHONY: release test live_docs clean
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ Install documentation builder dependencies with `pip install '.[doc]'`.
Run `make html` from the terminal to generate PyDRex's documentation
(available in the `html` directory), including the API reference.
The homepage will be `html/index.html`.
Alternatively, run `make live_docs` to build and serve the documentation on a `localhost` port.
Follow the displayed prompts to open the live documentation in a browser.
It should automatically reload after changes to the source code.

## Contributing

Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ mesh = ["gmsh"] # Required for pydrex-mesh, doesn't play nice with CI.
ray = ["ray >= 2.0.0"] # Required for distributed-memory examples.
# These optional dependencies are only relevant for source distributions:
test = ["pytest"] # Minimal test dependencies, for CI.
doc = ["pdoc"] # Minimal html docs dependencies, for CI.
doc = ["pdoc", "setuptools-scm"] # Minimal html docs dependencies, for CI.
dev = [ # Full developer dependencies, including packaging and visualisation tools.
"build",
"mypy",
Expand All @@ -61,6 +61,7 @@ dev = [ # Full developer dependencies, including packaging and visualisation to
"pytest",
"pyvista",
"ruff",
"setuptools-scm",
"twine",
"uniplot",
]
Expand Down Expand Up @@ -92,13 +93,14 @@ exclude = ["initial_implementation*"]

# Some global pytest configuration settings, avoids having an extra pytest.ini file.
[tool.pytest.ini_options]
# NOTE: Do NOT use pytest log_cli option, see tests/conftest for logging setup.
# Only look in '.tests/`, don't search git subprojects/worktrees or anything like that,
# <https://github.com/pytest-dev/pytest/issues/10298>.
testpaths = ["tests"]
# --tb=short : Use short tracebacks, terminal scrollback is precious.
# --show-capture=no : Don't show captured output (stdout/stderr/logs) for failed tests.
addopts = "--tb=short --show-capture=no"
# --capture=fd : Capture sys.std{out,err} and file descriptors 1 and 2 by default, show if -v is given.
# --show-capture=no : Don't show captured output for failed tests, use -v instead.
# -p no:logging : Disable built-in logging plugin, we have our own.
addopts = "--tb=short --capture=fd --show-capture=no -p no:logging"

# Global linter and devtools settings.
[tool.ruff]
Expand Down
1 change: 0 additions & 1 deletion src/pydrex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@
to_indices2d,
to_spherical,
)
from pydrex.io import data, logfile_enable, read_scsv, save_scsv
from pydrex.minerals import (
OLIVINE_PRIMARY_AXIS,
OLIVINE_SLIP_SYSTEMS,
Expand Down
36 changes: 34 additions & 2 deletions src/pydrex/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,26 @@ class DeformationRegime(IntEnum):
"""Arbitrary upper-bound viscosity regime."""


# Because frozen=True doesn't guarantee recursive immutabillity,
# check hashability in a doctest to ensure that this is actually immutable.
# This also ensures that subclasses are immutable (see pydrex.mock).
# Remember to use tuples instead of lists for members.
@dataclass(frozen=True)
class DefaultParams:
"""Immutable record of default parameters for PyDRex.
Use `as_dict` to get a mutable copy.
>>> defaults = DefaultParams()
>>> # Evaluating hash() will raise an error if the argument is mutable.
>>> isinstance(hash(defaults), int)
True
>>> from collections.abc import Hashable
>>> isinstance(defaults.as_dict(), Hashable) # No longer supports hash().
False
"""

phase_assemblage: tuple = (MineralPhase.olivine,)
"""Mineral phases present in the aggregate."""
phase_fractions: tuple = (1.0,)
Expand Down Expand Up @@ -185,7 +203,7 @@ class DefaultParams:
but that is not yet implemented.
"""
disl_Peierls_stress: float = 2
disl_Peierls_stress: float = 2.0
"""Stress barrier in GPa for activation of dislocation motion at low temperatures.
- 2GPa suggested by [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151)
Expand Down Expand Up @@ -275,8 +293,22 @@ class DefaultParams:
"""

def __post_init__(self):
for k, v in self.__dataclass_fields__.items():
if v.type is not type(v.default):
raise ValueError(f"Illegal type for {self.__class__.__qualname__}.{k}")

def as_dict(self):
"""Return mutable copy of default arguments as a dictionary."""
"""Return mutable copy of default arguments as a dictionary.
The reverse operation is achieved simply by passing the dictionary
back into the class constructor:
>>> params_mutable = DefaultParams().as_dict()
>>> params_mutable["number_of_grains"] = 9999
>>> params_immutable = DefaultParams(**params_mutable)
"""
return asdict(self)


Expand Down
56 changes: 45 additions & 11 deletions src/pydrex/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,16 +767,50 @@ def data(directory):


@cl.contextmanager
def logfile_enable(path, level=logging.DEBUG, mode="w"):
"""Enable logging to a file at `path` with given `level`."""
logger_file = logging.FileHandler(resolve_path(path), mode=mode)
logger_file.setFormatter(
logging.Formatter(
"%(levelname)s [%(asctime)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def logfile_enable(path, level: str | int = logging.DEBUG, mode="w"):
"""Enable logging to a file at `path` with given `level`.
See the `pydrex.logger` documentation for examples.
Logging levels are documented here:
- <https://docs.python.org/3/library/logging.html#logging-levels>
"""
formatter = logging.Formatter(
"%(levelname)s [%(asctime)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger_file.setLevel(level)
_log.LOGGER.addHandler(logger_file)
# Path can be an io.TextIOWrapper or io.StringIO, for testing purposes.
logger_file: logging.StreamHandler | logging.FileHandler
if isinstance(path, (io.StringIO, io.TextIOWrapper)):
_log.debug("enabling logging at %s level to IO stream")
logger_file = logging.StreamHandler(path)
logger_file.setFormatter(formatter)
logger_file.setLevel(level)
_log.LOGGER.addHandler(logger_file)
else:
_log.debug("enabling logging at %s level to %s", level, path)
logger_file = logging.FileHandler(resolve_path(path), mode=mode)
logger_file.setFormatter(formatter)
logger_file.setLevel(level)
_log.LOGGER.addHandler(logger_file)
yield
if not isinstance(path, (io.StringIO, io.TextIOWrapper)):
logger_file.close()
_log.LOGGER.removeHandler(logger_file)


@cl.contextmanager
def log_cli_level(level: str | int, handler: logging.Handler = _log.CONSOLE_LOGGER):
"""Set console logging handler level for current context.
See the `pydrex.logger` documentation for examples.
Logging levels are documented here:
- <https://docs.python.org/3/library/logging.html#logging-levels>
"""
default_level = handler.level
handler.setLevel(level)
yield
logger_file.close()
handler.setLevel(default_level)
Loading

0 comments on commit 6ff7d88

Please sign in to comment.