Skip to content

Conversation

@yochem
Copy link

@yochem yochem commented Sep 29, 2025

As I described in #128 (comment), this PR changes the project structure to have the user interfacing code mostly in a .py file while keeping the core functionality in C.

Currently, the _spi.py file subclasses wraps the SpiDev class as defined in spidev_module.c (renamed to _cspi.c). From there functionality can be gradually lifted to the new _spi.SpiDev class.

This change would allow for easier contributions from other Python programmers and clearly separates the core functionality and the Pythonic interface to it. It's also easier to include things like type hints and automatic documentation generation. This would be the first steps to a more modern and maintainable version of the current spidev package.

If someone is able to test the current initial version (@fschrempf?) and confirm it works as expected, I can continue with gradually lifting more into the new SpiDev class.

Eventually, I think the C module doesn't have to define the SpiDev class anymore, but rather expose a couple of functions (like open() and close()) that are called from the SpiDev class in the python file. But my knowledge of Python C modules is limited -- need an expert opinion on this.

Overview:

Added:

  • Class argument path: enables open() w.o. args instead of open_path().
  • Class argument mode: no need to set mode later.
  • Class argument bits_per_word: no need to set it later.
  • Class argument max_speed_hz: no need to set it later.
  • Property closed: IOBase, returns True if the connection is opened (fd is set).
  • Method read(): IOBase, wraps readbytes()
  • Method readable(): IOBase, same as not self.closed.
  • Method writeable(): IOBase, same as not self.closed.
  • Method write(): IOBase, wraps writebytes2().
  • Method __str__(): String representation.
  • Method __repr__(): For print()ing.
  • Method __eq__(): Compare if both instances point to the same device (simply compares the path of both)

Changed:

  • fileno() raises a value error if the connection is not opened instead of returning -1. This is more in line with Python builtins.
  • open() without arguments can use either bus and device attributes or the path attribute to open the connection via open_path(). open() is removed from the C module.
  • Context manager opens the file if bus and device or path attribute are set.
  • Class argument client renamed to device.

Removed / deprecated:

  • Nothing yet!
  • Some deprecation warnings for old behavior:
    • SpiDev().open(bus, device)SpiDev(bus, device).open()
    • SpiDev().open_path(path)SpiDev(path=path).open()
    • SpiDev().readbytes()SpiDev().read()
    • SpiDev().writebytes()SpiDev().write()
    • SpiDev().writebytes2()SpiDev().write()

Breaking Changes:

  • fileno() raises ValueError instead of returning -1 if file is not opened.
  • mode property raises ValueError instead of TypeError when value is not between 0 and 3.
  • bits_per_word property raises ValueError instead of TypeError when value is not 8, 16 or 32.
  • Class argument client renamed to device.

@yochem yochem force-pushed the py-class branch 4 times, most recently from f1ef67f to 7f1ec9c Compare September 29, 2025 15:22
@yochem
Copy link
Author

yochem commented Sep 29, 2025

Questions:

  1. is the client argument in init() supposed to be the same as device in open()?
  2. bits_per_word should be between 8 and 32, can we restrict this to be either 8, 16 or 32 or not?

@fschrempf
Copy link
Collaborator

Good questions!

is the client argument in init() supposed to be the same as device in open()?

On first glance, it looks like, yes.

bits_per_word should be between 8 and 32, can we restrict this to be either 8, 16 or 32 or not?

I've never seen any other value being supported anywhere. Which doesn't necessarily mean such cases don't (and will never) exist, but IMHO it would be safe enough to restrict it to those three values.

This is the same behavior as other Python I/O stream's `fileno()`
method.

BREAKING CHANGE: ValueError instead of -1 if the SPI device connection
is not open.
BREAKING CHANGE: Raises ValueError if mode is not between 0 and 3.
Calling `open()` without arguments can use the instance attributes `bus`
and `device` or `path` to call `open(bus, device)` or `open_path(path`
from the super class respectively. Thus the following options are
available:

```python
SpiDev().open(0, 1)

SpiDev(bus=0, device=1).open()
SpiDev(bus=1, device=1).open(0, 0) # args to open() overwrite attributes

SpiDev(path='/dev/myspi').open() # calls open_path('/dev/myspi')
```
BREAKING CHANGE: bits_per_word raises a ValueError instead of a
TypeError when its value is not between 8 and 32.
Posed some issues with setting attributes of the super class and it's
harder to refactor later on when C implementation will be cleaned up.
- SpiDev().open(bus, device) --> SpiDev(bus, device).open()
- SpiDev().open_path(path)   --> SpiDev(path=path).open()
- SpiDev().readbytes()       --> SpiDev().read()
- SpiDev().writebytes()      --> SpiDev().write()
- SpiDev().writebytes2()     --> SpiDev().write()
More in line with Python conventions
@yochem yochem marked this pull request as ready for review October 1, 2025 20:38
@yochem
Copy link
Author

yochem commented Oct 1, 2025

This is ready for review. I'm quite happy with it's current state; AFAIA, it is backwards compatible except some minor acceptable changes (listed above, mostly different Errors raised for certain situations).

In it's current state their is quite some "boilerplate" code, e.g.:

@property
def read0(self) -> bool:
    """Read 0 bytes after transfer to lower CS if cshigh is set."""
    return self._cmod.read0

@read0.setter
def read0(self, value: bool, /) -> None:
    self._cmod.read0 = try_convert(value, bool, "read0")

This enables to lift its logic from the C module to the Python class eventually (maybe by someone more experienced with the C codebase).

There are a couple TODO's in there: this is mostly parts where my knowledge is lacking, would be great to see some suggestions there.

help(spidev.SpiDev)
class SpiDev(builtins.object)
 |  SpiDev(
 |      bus: 'int | None' = None,
 |      device: 'int | None' = None,
 |      *,
 |      path: 'StrPath | None' = None,
 |      mode: 'int | None' = None,
 |      bits_per_word: 'int | None' = None,
 |      max_speed_hz: 'int | None' = None,
 |      read0: 'bool | None' = None
 |  ) -> 'None'
 |
 |  TODO.
 |
 |  Examples:
 |      >>> SpiDev(0, 1) # connect to /dev/spidev0.1
 |
 |      >>> SpiDev(path='/dev/myspi') # connect to /dev/myspi
 |
 |  Methods defined here:
 |
 |  __del__(self) -> 'None'
 |
 |  __enter__(self) -> 'Self'
 |
 |  __eq__(self, other: 'object') -> 'bool'
 |      Return self==value.
 |
 |  __exit__(
 |      self,
 |      exc_type: 'type[BaseException] | None',
 |      exc_value: 'BaseException | None',
 |      traceback: 'TracebackType | None'
 |  ) -> 'None'
 |
 |  __init__(
 |      self,
 |      bus: 'int | None' = None,
 |      device: 'int | None' = None,
 |      *,
 |      path: 'StrPath | None' = None,
 |      mode: 'int | None' = None,
 |      bits_per_word: 'int | None' = None,
 |      max_speed_hz: 'int | None' = None,
 |      read0: 'bool | None' = None
 |  ) -> 'None'
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  __repr__(self) -> 'str'
 |      Return repr(self).
 |
 |  __str__(self) -> 'str'
 |      Return str(self).
 |
 |  close(self) -> 'None'
 |      Close the object from the interface.
 |
 |  fileno(self) -> 'int'
 |      Return the file descriptor if it exists.
 |
 |      Returns:
 |          int: File descriptor number.
 |
 |      Raises:
 |          ValueError: if the connection is not open.
 |
 |  open(self, bus: 'int | None' = None, device: 'int | None' = None) -> 'None'
 |      Connect to the SPI device special file.
 |
 |      If bus and device are provided it opens "/dev/spidev<bus.<device>". If
 |      path is provided it opens the SPI device at given path. Symbolic links
 |      are followed.
 |
 |      Args:
 |          bus: Bus number.
 |          device: Device number.
 |
 |      Raises:
 |          ValueError: If bus/device or path is not provided.
 |
 |  open_path(self, path: 'StrPath | None' = None) -> 'None'
 |      Open SPI device at given path.
 |
 |      Args:
 |          path: Path to SPI device.
 |
 |      Raises:
 |          IOError
 |
 |  read(self, size: 'int' = -1, /) -> 'list[int]'
 |      Read and return up to _size_ bytes.
 |
 |      If size is omitted or negative, 1 byte is read.
 |
 |      Returns:
 |          list[int]: _size_ number of bytes.
 |
 |      Raises:
 |          OSError: If device is closed.
 |
 |  readable(self) -> 'bool'
 |      Return True if the SPI device is currently open.
 |
 |  readbytes(self, length: 'int') -> 'list[int]'
 |
 |  write(self, b: 'Sequence[int] | Buffer', /) -> 'None'
 |      Write bytes to SPI device.
 |
 |      Accepts arbitrary large lists. If list size exceeds buffer size (read
 |      from /sys/module/spidev/parameters/bufsiz), data will be
 |      split into smaller chunks and sent in multiple operations.
 |
 |      Args:
 |          b: Sequence of bytes or Buffer to write.
 |
 |      Raises:
 |          OSError: If device is closed.
 |
 |  writeable(self) -> 'bool'
 |      Return True if the SPI connection is currently open.
 |
 |  writebytes(self, values: 'Sequence[int]') -> 'None'
 |
 |  writebytes2(self, values: 'Sequence[int] | Buffer') -> 'None'
 |
 |  xfer(
 |      self,
 |      values: 'Sequence[int]',
 |      speed_hz: 'int | None' = None,
 |      delay_usecs: 'int | None' = None,
 |      bits_per_word: 'int | None' = None
 |  ) -> 'list[int]'
 |      Performs an SPI transaction.
 |
 |      NOTE: Chip-select should be released and reactivated between blocks.
 |
 |      Args:
 |          values: Bytes to write.
 |          speed_hz: Speed to use.
 |          delay_usecs: Delay in microseconds between blocks.
 |          bits_per_word: Bits per word.
 |
 |      Returns:
 |          TODO
 |
 |  xfer2(
 |      self,
 |      values: 'Sequence[int]',
 |      speed_hz: 'int | None' = None,
 |      delay_usecs: 'int | None' = None,
 |      bits_per_word: 'int | None' = None
 |  ) -> 'list[int]'
 |      Performs an SPI transaction.
 |
 |      NOTE: Chip-select should be held active between blocks.
 |
 |      Args:
 |          values: Bytes to write.
 |          speed_hz: Speed to use.
 |          delay_usecs: Delay in microseconds between blocks.
 |          bits_per_word: Bits per word.
 |
 |      Returns:
 |          TODO
 |
 |  xfer3(
 |      self,
 |      values: 'Sequence[int]',
 |      speed_hz: 'int | None' = None,
 |      delay_usecs: 'int | None' = None,
 |      bits_per_word: 'int | None' = None
 |  ) -> 'tuple[int, ...]'
 |      Performs an SPI transaction.
 |
 |      Accepts arbitrary large lists. If list size exceeds buffer size (read
 |      from /sys/module/spidev/parameters/bufsiz), data will be split
 |      into smaller chunks and sent in multiple operations.
 |
 |      Args:
 |          values: Bytes to write.
 |          speed_hz: Speed to use.
 |          delay_usecs: Delay in microseconds between blocks.
 |          bits_per_word: Bits per word.
 |
 |      Returns:
 |          TODO
 |
 |  ----------------------------------------------------------------------
 |  Readonly properties defined here:
 |
 |  closed
 |      Return True if the connection is not opened.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object
 |
 |  bits_per_word
 |      Bits per word used in the xfer methods.
 |
 |  max_speed_hz
 |      Max speed (in Hertz).
 |
 |  mode
 |      SPI mode.
 |
 |      A two bit pattern of clock polarity and phase [CPOL|CPHA],
 |      min: 0b00 = 0, max: 0b11 = 3
 |
 |  read0
 |      Read 0 bytes after transfer to lower CS if cshigh is set.
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |
 |  __hash__ = None

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants