This document provides guidelines for project coding style and conventions.
- Line Length & Splitting
- Whitespace & Formatting
- Exception Handling
- Documentation
- Writing Style
- Markdown Documentation
- Code Organization
- Tests
- Type Annotations & Type System
- API Design & Conventions
If the entire signature fits on one line within the 100-character limit, keep it on one line.
If the signature must be split, use one parameter per line. Do not put multiple parameters on the same line.
Alignment Rule: Use the opening parenthesis ( as the anchor point.
- The first parameter starts immediately after the
( - All subsequent parameters must align vertically at the same column position (column of
(+ 1) - Each parameter goes on its own line
Examples:
# Good: All parameters fit on one line.
def short_func(param1: int, param2: str) -> bool:
# Good: Split with one parameter per line.
def long_function_name(self,
param1: int,
param2: str,
param3: bool = False) -> dict:
# Bad: Multiple parameters on one line when split.
def long_function_name(self, param1: int,
param2: str, param3: bool = False) -> dict:
# Bad: Incorrect alignment.
def long_function_name(self,
param1: int,
param2: str,
param3: bool = False) -> dict:If the return type annotation is long, move it to the next line using a backslash \ continuation.
Align the return type at a 4-character boundary so it ends near the 100-character line limit.
def read_multiple_features(self,
fnames: Sequence[str],
cpus: Sequence[int]) -> \
Generator[tuple[int, dict[str, FeatureValueType]], None, None]:When a function or method call does not fit on a single line, split it across multiple lines following the same alignment rules as method signatures.
Fit as many arguments as possible on the first line. Continuation lines align at the column of
( + 1.
Examples:
modules_iter = sysfs_io.read_paths_int(id_paths, what="module number",
val_if_not_found=None)
for cpu, (path, module), (_, siblings_str) in zip(cpus_to_read, modules_iter,
siblings_iter):Log messages follow the same alignment rules as function calls. If everything fits on one line, keep it on one line. When splitting, prefer moving all arguments to continuation lines. However, keeping some arguments on the first line is acceptable if it saves a line.
Examples:
# Good: Fits on one line.
_LOG.debug("Cached: Read: CPU%d: MSR 0x%x%s", cpu, regaddr, self._pman.hostmsg)
# Good: All arguments on the next line.
_LOG.debug("Remote: Read: MSR 0x%x from CPUs %s%s, the command is: %s",
regaddr, cpus_range, self._pman.hostmsg, cmd)
# Good: Some arguments on the first line to save a line.
_LOG.debug("Transaction %d: %s: %s: CPU%d: MSR 0x%x: 0x%x to '%s'%s, command: %s", index,
transaction_type, operation_type, cpu, addr, regval, path, self._pman.hostmsg, cmd)When an assert statement does not fit on one line, the continuation line aligns right after 'assert ' (indent + 7).
Examples:
# Good: Proper alignment (4 spaces indent + 7 = column 11).
assert list(noncomp_dies) == sorted(noncomp_dies), \
"The package numbers returned are not in ascending order"
# Good: Multi-line message with proper alignment.
assert result == expected, \
f"Bad result of bytesize({size}, decp={decp}):\n" \
f"expected '{expected}', got '{result}'"
# Bad: Incorrect alignment (off by one or more spaces).
assert names == ["subdir", "test.fifo", "test.socket"], \
f"sort_by='alphabetic' failed: {names}"
# Bad: Message on the same line when it would exceed 100 characters.
assert condition_is_true, "This is a very very long error message that goes way beyond the limit"Keep the first line as close to the 100-character limit as possible. Each continued line should be its own f-string, aligned with the opening quote.
Use \ continuation only outside parentheses (where omitting it would be a syntax error). Inside
parentheses, \ is not needed. Python handles implicit string concatenation automatically.
Examples:
# Good: Outside parens (return without parens), \ is required.
return f"\nThe '{fpath}' file{self._pman.hostmsg} indicates that the '{opt}' kernel " \
f"boot parameter is set, this may be the reason"
# Good: Inside parens (function argument), \ is not needed.
raise Error(f"The '{fpath}' file{self._pman.hostmsg} indicates that the '{opt}' kernel "
f"boot parameter is set, this may be the reason")
# Bad: First line too short (unnecessarily splits early).
return f"\nThe '{fpath}' file{self._pman.hostmsg} indicates that " \
f"the '{opt}' kernel boot parameter is set, this may be the reason"
# Bad: Exceeds 100 characters on one line.
return f"\nThe '{fpath}' file{self._pman.hostmsg} indicates that the '{opt}' kernel boot parameter is set, this may be the reason"
# Bad: Using \ inside parentheses (redundant).
raise Error(f"The '{fpath}' file{self._pman.hostmsg} indicates that the '{opt}' kernel " \
f"boot parameter is set, this may be the reason")Prefer double quotes. Use single quotes only when the string contains double quotes.
Always use spaces around arithmetic operators (+, -, *, /, //, %, **) and comparison
operators (==, !=, <, >, <=, >=). This applies even when operators are used inside array
indices or other expressions.
For the assignment operator (=), use spaces around it in regular assignments, but do NOT use
spaces when passing keyword arguments to functions.
Examples:
# Good: Spaces around operators in all contexts.
result = value + 1
matrix[fdx - 2][sdx - 2] + cost
# Good: Spaces around = in assignments, no spaces in keyword arguments.
result = 5
func(cpus=(1, 5))
obj.method(name="value", count=10)
# Bad: No spaces around operators.
result = value+1
matrix[fdx-2][sdx-2]+cost
# Bad: Spaces in keyword arguments.
obj.method(name = "value")Use one blank line between method definitions within a class. Do not use multiple blank lines.
Example:
def method_one(self):
"""First method."""
pass
def method_two(self):
"""Second method."""
passAlways include a blank line between a function/method docstring and its body, unless both the docstring and body are one-liners, in which case the blank line is optional.
Do not include trailing spaces at the end of lines. Ensure that files end with a single newline character.
When defining a dictionary with multiple key-value pairs, use one key-value pair per line unless the dictionary is very small and fits on one line.
Vertical alignment of values is optional. If you choose to align values, add spaces after the colon
:, not before it. This keeps the colons in a clean vertical line while padding the values.
Examples:
# Good: Aligned values with spaces after colon.
_PKGINFO: Final[dict[str, dict[str, str]]] = {
"fedora": _FEDORA_PKGINFO,
"cent_os": _FEDORA_PKGINFO,
}
# Good: No alignment.
_PKGINFO: Final[dict[str, dict[str, str]]] = {
"fedora": _FEDORA_PKGINFO,
"cent_os": _FEDORA_PKGINFO,
}
# Bad: Spaces before colon.
_PKGINFO: Final[dict[str, dict[str, str]]] = {
"fedora" : _FEDORA_PKGINFO,
"cent_os" : _FEDORA_PKGINFO,
}The project uses only Error and its subclasses (defined in Exceptions.py) as the exception
type. Standard Python exceptions like OSError are intercepted at the lowest levels and converted
to Error or an appropriate subclass. A non-Error exception escaping a pepc method is a bug.
When re-raising, two cases apply:
Same semantic type: use type(err)(...) to preserve the exact subclass. except Error
catches subclasses like ErrorNotFound; hardcoding Error(...) would lose that.
# Wrong: loses the original subclass.
except Error as err:
raise Error(f"Failed to do X:\n{err.indent(2)}") from err
# Correct: preserves the original subclass.
except Error as err:
raise type(err)(f"Failed to do X:\n{err.indent(2)}") from errDeliberate type change: use the target type directly and document it in Raises:.
except ErrorNotFound as err:
raise ErrorNotSupported(f"Feature not available:\n{err.indent(2)}") from errWhen an error condition should never occur during correct program execution and its presence
indicates a programming bug, prefix the exception message with BUG:.
raise Error(f"BUG: Unexpected state '{state}'")Do not add blank lines before except:, finally:, or else: clauses.
Use Google style for docstrings.
Structure:
-
First paragraph: Summary of the method's purpose, written in imperative voice. Can be a
single line or multiple lines. Start with an imperative verb (e.g., "Return", "Check", "Yield", "Test").
-
Subsequent paragraphs (rarely needed): Additional descriptive paragraphs should be rarely
needed. Use structured sections (
Args:,Returns:,Yields:,Notes:,Examples:) instead, as they provide better organization and readability. Only add additional paragraphs if the information cannot be properly expressed in structured sections.Prefer a short
Notes:bullet list over a prose paragraph, or add anExamples:section. A few concrete examples are often clearer to readers than a lengthy description. -
Notes section (optional): Use
Notes:(plural) for additional details formatted as a bulletlist. Common uses:
- Validation requirements (e.g., "Methods do not validate the 'cpus' argument")
- Thread-safety information
- Performance considerations
- Other important caveats
-
Args/Returns/Yields/Raises sections:
-
Args:is mandatory if the method has parameters (exceptself) -
Returns:is mandatory if the method returns a value (other thanNone) -
Yields:is mandatory if the method is a generator -
Raises:is mandatory for public methods that raise exceptions (except the baseErrorclass), optional for private methods
-
Guidelines:
-
For one-line docstrings, keep the closing
"""on the same line (e.g.,"""Return the value.""") -
For multi-line docstrings, put both the opening and closing
"""on their own separate lines -
Do not repeat information that is already clear from the function signature and summary
-
Do not repeat the type in
Args:,Returns:, orYields:sections. The type is already expressed in the function signature annotations. -
Prefer putting additional details in the
Notes:section as bullet points rather than inparagraph form
-
Skip detailed description paragraphs if the summary and Args/Returns/Notes sections are
sufficient
-
Use single quotes (not backticks) to reference variable names, function names, and similar
identifiers in docstrings. Example: "See 'close()' for details" or "Returns the 'cpu' value".
-
For helper functions that implement or wrap another function, reference the main function's
arguments instead of repeating them when appropriate. Example: "Arguments are the same as in 'main_function()'." This avoids repetition when argument lists are long or obvious from context.
Examples:
# Good: Concise single-line summary.
def get_cpu_count(self) -> int:
"""Return the number of CPUs on the system."""
# Good: Using imperative voice, concise summary.
def read_msr(self, regaddr: int, cpus: Iterable[int]) -> Generator[tuple[int, int], None, None]:
"""
Read MSR register from specified CPUs and yield the values. Both local and remote execution
are supported.
Args:
regaddr: The MSR register address to read.
cpus: The CPU numbers to read from.
Yields:
Tuples of (CPU number, register value).
"""
# Bad: Redundant body text, using "this method" instead of imperative voice.
def get_cpu_count(self) -> int:
"""
Return the number of CPUs on the system.
This method returns the number of CPUs that are available on the system.
Returns:
The number of CPUs.
"""Do not document Error itself, it is the common base class. Document subclasses that a public
method can raise. For private methods, document exceptions when helpful.
Use declarative statements, not conditional "if" clauses:
Example:
# Good.
Raises:
ErrorNotSupported: The file does not exist.
# Bad.
Raises:
ErrorNotSupported: If the file does not exist.Continuation lines in structured docstring elements (Args:, Raises:, etc.) align with the text
after the colon (continuation column = colon column + 2).
Examples:
# Good: Continuation lines align with text after colon.
def write(self, regaddr: int, regval: int, cpus: Sequence[int]):
"""
Write a value to an MSR on specified CPUs.
Args:
regaddr: The address of the MSR to write to.
regval: The value to write to the MSR.
cpus: CPU numbers to write the MSR on (the caller must validate CPU
numbers).
Raises:
ErrorVerifyFailedPerCPU: If verification is enabled and the read-back value does
not match the written value. The 'cpu' attribute of the
exception will contain the CPU number where the
verification failed.
"""
# Bad: Continuation lines do not align properly.
def write(self, regaddr: int, regval: int, cpus: Sequence[int]):
"""
Write a value to an MSR on specified CPUs.
Args:
regaddr: The address of the MSR to write to.
regval: The value to write to the MSR.
cpus: CPU numbers to write the MSR on (the caller must validate CPU
numbers). # Wrong: not aligned with "CPU"
Raises:
ErrorVerifyFailedPerCPU: If verification is enabled and the read-back value does
not match the written value. # Wrong: not aligned with "If"
"""Use imperative voice in docstrings and comments. Start with verbs like "Provide", "Return", "Check", "Initialize".
Examples:
# Good: Imperative voice.
"""Provide helper methods for checking if tools are available on the system."""
"""Return the OS information dictionary."""
"""Check if the tool is installed on the target system."""
# Bad: Declarative voice.
"""This module provides helper methods for checking if tools are available."""
"""This method returns the OS information dictionary."""All comments should end with a period (.). Do not use semicolons (;) as punctuation in
comments or messages. Use a comma (,) or a period (.) instead.
Comments should explain why, not what. The code itself shows what happens. Only describe the how when it is non-obvious.
Prefer placing comments on a separate line before the code they describe, rather than appending
them to the end of a code line. Short TODO/note annotations on from __future__ imports or similar
one-off markers are fine inline.
# WRONG: restates what the code already says.
# Flush the file to disk.
fobj.flush()
# RIGHT: explains why.
# Paranoid flush to minimise the chance of data loss on crash.
fobj.flush()
# OK: TODO-style annotation on a special import line.
from __future__ import annotations # Remove when switching to Python 3.10+.When referring to a function, method, or other identifier in a comment, use single quotes with
parentheses for callables: 'configure()', 'close()'. For non-callables, use single quotes
without parentheses: 'outdir', '_pman'.
Do not use separator or divider comments to split a file or class into visual sections:
# WRONG: separator comment.
# ---------- Set functions ------------------
# WRONG: any variation of a visual divider.
# ============================================
# ############################################If a file or class feels large enough to need such separators, treat it as a signal that the code should be refactored into smaller files or classes instead.
Every log and exception message should start with a capital letter. After each colon in a message, the next word should also be capitalized. One-line messages should not have a trailing period; multi-line messages should use periods.
Examples:
# Good: Capital letters after each colon.
_LOG.debug("Local: Read: CPU%d: MSR 0x%x: 0x%x", cpu, regaddr, val)
_LOG.debug("Transaction: Remote: Write: Executing command%s: %s", hostmsg, cmd)
raise Error(f"BUG: Invalid CPU number: {cpu}. Valid range is 0-{max_cpu}.")
# Bad: Lowercase after colons.
_LOG.debug("Local: read: CPU%d: msr 0x%x: 0x%x", cpu, regaddr, val)These rules apply to all prose in the project: code comments, docstrings, commit messages, and Markdown documentation.
Do not use em-dashes (—). Replace with a comma, a period, or restructure the sentence.
Examples:
# Wrong.
The tool reads the register — then writes it back.
# Right.
The tool reads the register, then writes it back.
Do not use semicolons (;) as sentence punctuation. Use a comma or a period instead.
Use logical punctuation: commas and periods go outside closing quotation marks, not inside.
Examples:
# Wrong (American style).
Write "SEPT," not "EPC".
# Right (logical punctuation).
Write "SEPT", not "EPC".
Use a single space between sentences, not two.
Use backticks for:
-
Tool and command names:
pepc,git,pip3,uv. -
Subcommand names:
pepc pstates,pepc cstates. -
Option names:
--cpus,--max-freq,-H. -
File and directory paths:
~/.bashrc,tests/emul-data/,/sys/devices/.... -
Environment variable names:
PATH,MANPATH. -
Code identifiers: function names, class names, and similar symbols, e.g.,
read_msr(),ErrorNotFound.
Do not split a backtick-quoted span across lines. Keep the opening and closing backtick on the same line. If adding the span would push the line past 100 characters, break the line before the opening backtick.
Do not split a [text](url) link across lines. After the link ends, continue text on the same
line and only break when the line would exceed 100 characters. If the link itself already reaches
or exceeds 100 characters, break the line immediately before or after the link.
Correct:
Refer to the
[Performance Level to Frequency Mapping](#performance-level-to-frequency-mapping) section.
See [Pepc User Guide: Uncore](guide-uncore.md)
for details.Incorrect:
Refer to the [Performance Level to
Frequency Mapping](#performance-level-to-frequency-mapping) section.Always surround a list with a blank line above and below it.
Correct:
Some text.
- Item one.
- Item two.
More text.Incorrect:
Some text.
- Item one.
- Item two.
More text.Hard-wrap prose lines at 100 characters. Fill each line as close to 100 characters as possible before wrapping to the next line. Do not leave lines short when more text can fit.
Code blocks are excluded: command output and file paths cannot always be wrapped.
Capitalize section headings in title case.
Examples:
# Good.
### What the MC Checks
# Bad.
### What the MC checksAlways specify a language tag on fenced code blocks. Never use a bare ``` with no tag.
Common tags: python, c, bash, yaml, markdown, text. Use text for plain output,
command examples, or anything that has no better-fitting language.
Examples:
# Good.
```python
print("hello")
```
# Good: plain output with no syntax.
```text
INFO: Converting: foo.pdf
Written: foo.md
```
# Bad: no language tag.
```
print("hello")
```The __init__() method should be defined at the top of the class, immediately after the class
docstring. The close() method and other special methods (e.g., __enter__, __exit__) should
go after __init__(). Regular methods should be defined after the special methods.
Avoid __del__(). See Resource Cleanup for details.
The general order of methods in a class should be that the inner methods are defined before the outer methods. For example, if a method A calls method B, then method B should be defined before method A.
All private symbols must start with an underscore _. This applies to:
- Methods and functions
- Global variables, including constants
- Types, including
TypedDictdefinitions
Examples:
# Private constant
_DEFAULT_TIMEOUT = 30
# Private TypedDict
if typing.TYPE_CHECKING:
class _DeviceInfoTypedDict(TypedDict):
"""
... Reasonable Docstring ...
"""
# Public constant
MAX_RETRIES = 5Class attributes follow the same naming convention as instance attributes: snake_case. Do not use
ALL_CAPS for class attributes, even if they are conceptually constant. ALL_CAPS is reserved for
module-level constants.
# Wrong: ALL_CAPS class attribute.
class _PDFConverter:
EXTENSIONS = frozenset({".pdf"})
# Right: snake_case class attribute.
class _PDFConverter:
extensions = frozenset({".pdf"})Prefer importing a module and referencing its symbols via the module name rather than importing
individual symbols with from X import Y. This makes the origin of each symbol obvious at the
call site and avoids ambiguity.
Good:
from pepclibs.helperlibs import Logging
_LOG = Logging.getLogger(...)Bad:
from pepclibs.helperlibs.Logging import getLogger
_LOG = getLogger(...)Exceptions:
from X import Error, ErrorNotSupported: exception classes are always imported by name, because writingExceptions.Errorat every raise/catch site is noisy.from typing import IO, Generator, Sequence: typing symbols are always imported by name, never usetyping.IO,typing.Generator, etc.- Established ecosystem conventions, e.g.
import pandas as pd/pd.DataFrame, are fine. - Do not rename modules on import (e.g.
from X import _Mod as mod) unless there is a specific reason. Use the real module name at the call site.
When importing multiple symbols from the same module, use multiple separate import statements
rather than parentheses for multi-line imports. This makes imports easier to grep.
Pack as many names as fit within the 100-character line limit per statement. When they don't all
fit, start a new from X import statement for the remainder, again packing as many as fit.
Good:
from pepclibs.helperlibs.Exceptions import Error, ErrorNotSupported, ErrorPermissionDenied
from pepclibs.helperlibs.Exceptions import ErrorPerCPUBad:
from pepclibs.helperlibs.Exceptions import (Error, ErrorNotSupported, ErrorPermissionDenied,
ErrorPerCPU)Also bad (one name per line):
from pepclibs.helperlibs.Exceptions import Error
from pepclibs.helperlibs.Exceptions import ErrorNotSupported
from pepclibs.helperlibs.Exceptions import ErrorPermissionDenied
from pepclibs.helperlibs.Exceptions import ErrorPerCPUFor function/method parameters, prefer the least restrictive type that is sufficient:
- Use
Iterable[T]when the parameter is only iterated once. - Use
Sequence[T]when random access or multiple iterations are needed. - Use
list[T]ortuple[T, ...]only when mutation or a concrete type is required.
Avoid annotating parameters as list when Iterable or Sequence would work, it unnecessarily
restricts callers.
If a method does not return anything (i.e., it returns None), do not include a return type
annotation at all.
Import typing utilities (cast, TypedDict, Generator, Sequence, etc.) under the
typing.TYPE_CHECKING guard to avoid runtime overhead.
import typing
if typing.TYPE_CHECKING:
from typing import cast, Generator, TypedDict, Sequence
from some.module import SomeType
# Usage of cast in code.
if typing.TYPE_CHECKING:
value = cast(int, some_value)
else:
value = some_valuePrefer TypedDict over plain dict when the structure is well-defined. Name with a TypedDict
suffix. Include a docstring with summary and Attributes: section.
Use total=False when building dictionaries key by key.
Examples:
# Good: Using TypedDict with total=False.
import typing
if typing.TYPE_CHECKING:
from typing import TypedDict
class PCIInfoTypedDict(TypedDict, total=False):
"""PCI device information.
Attributes:
addr: PCI device address.
vendorid: Vendor ID of the PCI device.
devid: Device ID of the PCI device.
"""
addr: str
vendorid: int
devid: intPrefer tuple over list and frozenset over set for collections that should not be modified
after creation.
When passing a small collection of items (e.g., CPU numbers), prefer a tuple over a list:
def read(self,
regaddr: int,
cpus: Iterable[int] | Literal["all"] = "all",
verify: bool = False) -> Generator[tuple[int, int], None, None]:When calling this method only for CPU 0, use a tuple for the cpus argument:
for cpu, val in self.read(regaddr=0x4E70, cpus=(0,), verify=True):
...Avoid None as a default value when a same-type sentinel works:
- Strings:
"" - Integers:
-1 - Sequences:
()
Example:
def process_data(self, name: str = "", count: int = -1, cpus: Sequence[int] = ()):
if not name:
name = "default"
if count == -1:
count = 10
if not cpus:
cpus = self.get_all_cpus()Prefer Trivial.str_to_int(), Trivial.str_to_float(), or Trivial.str_to_num() over built-in
int()/float(). Use built-ins only when the string is already verified to be valid.
Use keyword arguments when calling methods, in the same order as the signature:
def read(self,
regaddr: int,
cpus: Iterable[int] | Literal["all"] = "all",
verify: bool = False) -> Generator[tuple[int, int], None, None]:Call with keyword arguments:
for cpu, val in self.read(regaddr=0x4E70, cpus=cpus, verify=True):
...Use raise SystemExit(code) instead of sys.exit(). Always pass the exit code explicitly:
# Wrong.
sys.exit(0)
# Right.
raise SystemExit(0)Prefer pathlib.Path for path manipulation over the os and os.path modules. Use Path
methods directly where possible:
# Prefer.
path = Path("/some/dir") / "file.txt"
if path.is_dir():
path.mkdir(parents=True)
# Avoid.
path = os.path.join("/some/dir", "file.txt")
if os.path.isdir(path):
os.makedirs(path)This is not a hard rule. When pathlib.Path is not the clearest choice, for example when a
library requires plain strings, or when manipulating a path with regex is simpler, prefer
whichever approach is cleaner.
Use close() for resource cleanup, not __del__(). __del__() is timing non-deterministic,
silently swallows exceptions, and may run during interpreter shutdown in an undefined state. Only
use it when there is a specific, well-justified reason. Document the justification in a comment.
# Wrong.
class Reader:
def __init__(self):
self._fobj: IO[str] | None = open("data.txt")
def __del__(self):
if getattr(self, "_fobj", None):
self._fobj.close()
self._fobj = None
# Right.
class Reader:
def __init__(self):
self._fobj: IO[str] | None = open("data.txt")
def close(self):
if self._fobj is not None:
self._fobj.close()
self._fobj = NoneInherit from ClassHelpers.SimpleCloseContext (from pepclibs) instead of defining
__enter__() and __exit__() manually. It implements them by calling close(), so defining
close() is all that is needed.
# Wrong.
class Reader:
def __enter__(self):
return self
def __exit__(self, *_):
self.close()
# Right.
class Reader(ClassHelpers.SimpleCloseContext):
pass # __enter__ and __exit__ are provided by SimpleCloseContext.Avoid calling next() directly on generators. It raises StopIteration if the generator is
exhausted, and unhandled StopIteration propagates silently in unexpected ways.
For extracting a single value from a generator, use a for loop with return and a trailing
raise:
# Wrong: raises StopIteration if the generator yields nothing.
_, val, mname = next(self._get_epp_or_epb((cpu,), mnames))
return val, mname
# Right: explicit failure path.
for _, val, mname in self._get_epp_or_epb((cpu,), mnames):
return val, mname
raise Error(f"BUG: No EPP/EPB value yielded for CPU {cpu}")When an empty iterator should simply return a default or bail out (no error), use
for/break/else — the else block executes only when no break was hit (i.e., the iterator
was empty):
for _, driver in self._get_driver(cpus, mnames):
break
else:
return "" # Iterator was empty.
if driver != "intel_pstate":
return ""If next() is unavoidable, either pass a default value as the second argument
(next(it, None)) and check the result, or catch StopIteration explicitly.
Use ClassHelpers.close() in close() methods to release owned and borrowed attributes.
close_attrs: attributes the class owns. Theirclose()is called, then the attribute is set toNone.unref_attrs: attributes the class borrows (created externally). Only set toNone, noclose()called.
Benefits over open-coded cleanup:
- Tolerates attributes that were never set (
__init__raised partway through): usesgetattr(..., None)internally. - The
_close_{attr}flag lets you suppressclose()for a specific attribute without changing the call site, which is useful when ownership is conditional. - Logs a warning if an attribute name is not found on the object, catching typos at cleanup time.
Drawback: attribute names are plain strings, so IDEs and static analysis tools cannot follow them.
For simple classes, open-coded cleanup is often just as clear. Use ClassHelpers.close() anyway.
The project follows this pattern consistently, and consistency has its own value.
import typing
from pepclibs.helperlibs import LocalProcessManager
if typing.TYPE_CHECKING:
from pepclibs.helperlibs.ProcessManager import ProcessManagerType
class Reader(ClassHelpers.SimpleCloseContext):
def __init__(self, pman: ProcessManagerType | None = None):
# When 'pman' is 'None', create a local process manager and own it (close on exit).
# When provided by the caller, borrow it (do not close on exit).
self._close_pman = pman is None
if pman:
self._pman = pman
else:
self._pman = LocalProcessManager.LocalProcessManager()
def close(self):
# '_close_pman' controls whether 'ClassHelpers.close()' calls 'self._pman.close()'.
ClassHelpers.close(self, close_attrs=("_pman",))Apply the same callee-before-caller principle as Class Layout: define a function
before the functions that call it. For test files this means each private helper _test_foo()
immediately precedes its public test_foo(). Do not group all helpers at the top and all public
functions at the bottom.
# WRONG: helpers and public functions separated.
def _test_foo(params): ...
def _test_bar(params): ...
def test_foo(params):
_test_foo(params)
def test_bar(params):
_test_bar(params)
# RIGHT: each helper immediately precedes its public caller.
def _test_foo(params): ...
def test_foo(params):
_test_foo(params)
def _test_bar(params): ...
def test_bar(params):
_test_bar(params)Use pytest.skip(), not return, when a test cannot run because the platform does not support
the required feature or topology. This ensures the test shows as skipped (with a reason) in the
report rather than silently passing.
# WRONG: test silently passes, no indication it was skipped.
def test_foo(params):
if not feature_supported:
return
...
# RIGHT: test is reported as skipped with a clear reason.
def test_foo(params):
if not feature_supported:
pytest.skip("Feature X is not supported on this platform")
...pytest.skip() raises internally, so it works from both public test functions and private helpers
called by them.