-
Notifications
You must be signed in to change notification settings - Fork 4
Simplify property and add type hints #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
59 commits
Select commit
Hold shift + click to select a range
c84a950
Add documentation pages on OpenAPI/TD and Descriptors
rwb27 a914ac2
First pass at splitting up ThingProperty
rwb27 da345fb
Update the rest of the library to use the new names
rwb27 54b19f0
Update examples and test suite
rwb27 7ad0755
Fix test_property tests
rwb27 a4e44d4
Fix last remaining mypy error
rwb27 95f44ab
Fix a typo in example code
rwb27 2212e66
Restore functional settings
rwb27 2809992
Added a tutorial page on properties and things
rwb27 90d5de8
Add typing tests for thing definition code.
rwb27 847c192
Tighten up type checking of properties
rwb27 25dbdfb
Make `default` a keyword-only argument for `setting` and `property`
rwb27 a64bc24
Re-lock dependencies under Windows, Python 3.10
rwb27 1b91ae3
Fix codespell config and some spelling errors
rwb27 3c02865
Fixed flake8 errors
rwb27 587829f
Update tests and docs to use a named `default` arg to `lt.property`
rwb27 029343f
More helpful error if a non-callable value is passed as the positiona…
rwb27 14d3dd9
Add to external linter codes to stop ruff warning
rwb27 551e308
Make CI compatible with Python 3.10 and Linux
rwb27 1d37e80
Implement, and test, docstring-after-the-property syntax.
rwb27 6927ce3
Documentation improvements from review and codespell
rwb27 315725b
Fix test of BaseDescriptor and docs
rwb27 efc330e
Fix typing tests foder in CI
rwb27 531e87e
Comments on modified settings test
rwb27 ab54ce7
Add a comment, and test both object and class attributes
rwb27 c92500f
Improve test_properties.py
rwb27 c6b24e7
Add comments/docstrings about cacheing in get_class_attribute_docstrings
rwb27 b3a8c6e
Add a docstring for the example class.
rwb27 b023023
Add docstrings to the example Thing.
rwb27 70ab0ee
Fix spelling error
rwb27 4659d59
Improve tests of base_descriptor
rwb27 77f154c
Add a full stop
rwb27 c5f98d6
Apply suggestions from code review
rwb27 9c1de79
Apply suggestions from code review
rwb27 8b18d75
Better error if default_factory is not callable.
rwb27 43e436f
Remove deleter
rwb27 f757a95
Apply suggestions from code review
rwb27 ab7a631
Improve docstrings
rwb27 089b98e
Add tests of `property` and `setting`
rwb27 416e9b0
Test what happens if getter and setter have different names.
rwb27 354cdfb
Guard against BaseDescriptor being used twice.
rwb27 0473952
WIP: Add an exemption to allow properties with differently named setters
rwb27 25a42e5
Better handling of differently named setters.
rwb27 55dcb00
Eliminate DocstringToMessage in errors
rwb27 df8c5a9
Better documentation of type test.
rwb27 b2f597e
Remove unused typevar
rwb27 f976ac1
Better comments/docstrings
rwb27 9ed747a
Add more detailed tests of settings
rwb27 309aa8a
Add a test for dictsetting
rwb27 f30f425
Add comment on cache
rwb27 f3963df
Fix docstring format
rwb27 4cd9873
Fix spelling errors
rwb27 7f4e8dd
Fix test imports
rwb27 f988566
Fix tests of basedescriptor errors on old Python versions.
rwb27 c092f9c
Use raises_or_is_caused_by in test_properties
rwb27 124d963
Added a test for redefinition errors when getter/setter names match.
rwb27 316a775
Get rid of unnecessary #noqa
rwb27 8ce08ce
Re-lock dependencies without flake8-docstrings
rwb27 45de736
Rename `thing_property` to `properties`
rwb27 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| .. _gen_docs: | ||
|
|
||
| Generated documentation | ||
| ======================= | ||
|
|
||
| LabThings describes its HTTP API in two ways: with a :ref:`wot_td` and with an OpenAPI_ document. | ||
|
|
||
| .. _openapi: | ||
|
|
||
| OpenAPI | ||
| ------- | ||
|
|
||
| OpenAPI_ is a standard way to describe an HTTP interface. It lists all of the possible HTTP requests that may be made, along with a description of each one, and a description of the possible responses. | ||
|
|
||
| .. _gen_td: | ||
|
|
||
| Thing Description | ||
| ----------------- | ||
|
|
||
| Each :ref:`wot_thing` is documented by a Thing Description, which is a JSON document describing all of the ways to interact with that Thing (:ref:`wot_affordances`\ ). The WoT_ standard defines the `Thing Description`_ and includes a JSON Schema against which it may be validated. | ||
|
|
||
| Thing Description documents are higher-level than OpenAPI_ and focus on the capabilities of the Thing. For example, they include a list of properties, where each action is described only once. LabThings treats the Thing Description as your public API, and as a general rule anything not described in the Thing Description is not available over HTTP or to a `.DirectThingClient`\ . | ||
|
|
||
| Comparison of Thing Description and OpenAPI | ||
| ------------------------------------------- | ||
|
|
||
| Thing Description aims to be a neat way to describe the capabilities of a Thing, while OpenAPI focuses on detailed documentation of every possible interaction with the server. Thing Description is a newer and less well adopted standard that's specific to the Web of Things, while OpenAPI has been around for a while and is widely used and understood as a general-purpose API description. | ||
|
|
||
| OpenAPI describes each HTTP endpoint individually. There are usually more HTTP endpoints than there are :ref:`wot_affordances` because a Property may have two endpoints, one to read its value and one to write it. Actions usually correspond to several endpoints; one to invoke the action, one to check the action's status, one to cancel it, and another to retrieve its output once it has finished. In principle, client code based on a Thing Description should be more meaningful because related endpoints can be grouped together into properties or actions that correspond to structures in the programming language (like methods and properties in Python). However, OpenAPI is a much more widely adopted standard and so both forms of documentation are generated by LabThings-FastAPI. | ||
|
|
||
| .. _WoT: https://www.w3.org/WoT/ | ||
| .. _Thing Description: https://www.w3.org/TR/wot-thing-description/ | ||
| .. _OpenAPI: https://www.openapis.org/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| See Also | ||
| ======== | ||
|
|
||
| LabThings-FastAPI makes quite heavy use of a few key concepts from external libraries, including `fastapi`, `pydantic`, and of course Python's core library. This page attempts to summarise these, and also acts as a useful place for docstrings to link to, so we can avoid repetition. | ||
|
|
||
| .. _descriptors: | ||
|
|
||
| Descriptors | ||
| ----------- | ||
|
|
||
| Descriptors are a way to intercept attribute access on an object. By default, attributes of an object are just variables - so an object called ``foo`` might have an attribute called ``bar``, and you may read its value with ``foo.bar``, write its value with ``foo.bar = "baz"``, and delete the attribute with ``del foo.bar``. If ``foo`` is a descriptor, Python will call the ``__get__`` method of that descriptor when it's read and the ``__set__`` method when it's written to. You have quite probably used a descriptor already, because the built-in `~builtins.property` creates a descriptor object: that's what runs your getter method when the property is accessed. The descriptor protocol is described with plenty of examples in the `Descriptor Guide`_ in the Python documentation. | ||
|
|
||
| In LabThings-FastAPI, descriptors are used to implement :ref:`wot_actions` and :ref:`wot_properties` on `.Thing` subclasses. The intention is that these will function like standard Python methods and properties, but will also be available over HTTP, along with automatic documentation in the :ref:`wot_td` and OpenAPI documents. | ||
|
|
||
| There are a few useful notes that relate to many of the descriptors in LabThings-FastAPI: | ||
|
|
||
| * Descriptor objects **may have more than one owner**. As a rule, a descriptor object | ||
| (e.g. an instance of `.DataProperty`) is assigned to an attribute of one `.Thing` subclass. There may, however, be multiple *instances* of that class, so it is not safe to assume that the descriptor object corresponds to only one `.Thing`. This is why the `.Thing` is passed to the ``__get__`` method: we should ensure that any values being remembered are keyed to the owning `.Thing` and are not simply stored in the descriptor. Usually, this is done using `.WeakKeyDictionary` objects, which allow us to look up values based on the `.Thing`, without interfering with garbage collection. | ||
|
|
||
| The example below shows how this can go wrong. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| class BadProperty: | ||
| "An example of a descriptor that has unwanted behaviour." | ||
| def __init__(self): | ||
| self._value = None | ||
|
|
||
| def __get__(self, obj): | ||
| return self._value | ||
|
|
||
| def __set__(self, obj, val): | ||
| self._value = val | ||
|
|
||
| class BrokenExample: | ||
| myprop = BadProperty() | ||
|
|
||
| a = BrokenExample() | ||
| b = BrokenExample() | ||
|
|
||
| assert a.myprop is None | ||
| b.myprop = True | ||
| assert a.myprop is None # FAILS because `myprop` shares values between a and b | ||
|
|
||
| * Descriptor objects **may know their name**. Python calls ``__set_name__`` on a descriptor if it is available. This allows the descriptor to know the name of the attribute to which it is assigned. LabThings-FastAPI uses the name in the URL and in the Thing Description. When ``__set_name__`` is called, the descriptor **is also passed the class that owns it**. This allows us to check for type hints and docstrings that are part of the class, rather than part of the descriptor. | ||
| * There is a convention that descriptors return their value when accessed as an instance attribute, but return themselves when accessed as a class attribute (as done by `builtins.property`). LabThings adheres to that convention. | ||
|
|
||
| .. _`Descriptor Guide`: https://docs.python.org/3/howto/descriptor.html |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| .. _tutorial_properties: | ||
|
|
||
| Properties | ||
| ========================= | ||
|
|
||
|
|
||
|
|
||
| Properties are values that can be read from and written to a Thing. They are used to represent the state of the Thing, such as its current temperature, brightness, or status. :ref:`wot_properties` are a key concept in the Web of Things standard. | ||
|
|
||
| LabThings implements properties in a very similar way to the built-in Python `~builtins.property`. The key difference is that defining an attribute as a `.property` means that the property will be listed in the :ref:`gen_td` and exposed over HTTP. This is important for two reasons: | ||
|
|
||
| * Only properties declared using `.property` (usually imported as ``lt.property``) can be accessed over HTTP. Regular attributes or properties using `builtins.property` are only available to your `.Thing` internally, except in some special cases. | ||
| * Communication between `.Thing`\ s within a LabThings server should be done using a `.DirectThingClient` class. The purpose of `.DirectThingClient` is to provide the same interface as a `.ThingClient` over HTTP, so it will also only expose functionality described in the Thing Description. | ||
|
|
||
| You can add properties to a `.Thing` by using `.property` (usually imported as ``lt.property``). | ||
|
|
||
| Data properties | ||
| ------------------------- | ||
|
|
||
| Data properties behave like variables: they simply store a value that is used by other code on the `.Thing`. They are defined similarly to fields in `dataclasses` or `pydantic` models: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import labthings_fastapi as lt | ||
|
|
||
| class MyThing(lt.Thing): | ||
| my_property: int = lt.property(default=42) | ||
|
|
||
| The example above defines a property called `my_property` that has a default value of `42`. Note the type hint `int` which indicates that the property should hold an integer value. This is important, as the type will be enforced when the property is written to via HTTP, and it will appear in :ref:`gen_docs`. By default, this property may be read or written to by HTTP requests. If you want to make it read-only, you can set the `readonly` parameter to `True`: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| class MyThing(lt.Thing): | ||
| my_property: int = lt.property(default=42, readonly=True) | ||
|
|
||
| Note that the ``readonly`` parameter only affects *client* code, i.e. it may not be written to via HTTP requests or `.DirectThingClient` instances. However, the property can still be modified by the Thing's code, e.g. in response to an action or another property change as ``self.my_property = 100``. | ||
|
|
||
| It is a good idea to make sure there is a docstring for your property. This will be used in the :ref:`gen_docs`, and it will help users understand what the property is for. You can add a docstring to the property by placing a string immediately after the property definition: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| class MyThing(lt.Thing): | ||
| my_property: int = lt.property(default=42, readonly=True) | ||
| """A property that holds an integer value.""" | ||
|
|
||
| You don't need to include the type in the docstring, as it will be inferred from the type hint. However, you can include additional information about the property, such as its units or any constraints on its value. | ||
|
|
||
| Data properties may be *observed*, which means notifications will be sent when the property is written to (see below). | ||
|
|
||
| Functional properties | ||
| ------------------------- | ||
|
|
||
| It is also possible to have properties that run code when they are read or written to. These are called functional properties, and they are defined using the `lt.FunctionalProperty` class. They might communicate with hardware (for example to read or write a setting on an instrument), or they might perform some computation based on other properties. They are defined with a decorator, very similarly to the built-in `property` function: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import labthings_fastapi as lt | ||
|
|
||
| class MyThing(lt.Thing): | ||
| my_property: int = lt.property(default=42) | ||
| """A property that holds an integer value.""" | ||
|
|
||
| @lt.property | ||
| def twice_my_property(self) -> int: | ||
| """Twice the value of my_property.""" | ||
| return self.my_property * 2 | ||
|
|
||
| The example above defines a functional property called `twice_my_property` that returns twice the value of `my_property`. The type hint `-> int` indicates that the property should return an integer value. When this property is read via HTTP, the code in the method will be executed, and the result will be returned to the client. As with `property`, the docstring of the property is taken from the method's docstring, so you can include additional information about the property there. | ||
|
|
||
| Functional properties may also have a "setter" method, which is called when the property is written to via HTTP. This allows you to perform some action when the property is set, such as updating a hardware setting or performing some computation. The setter method should take a single argument, which is the new value of the property: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import labthings_fastapi as lt | ||
|
|
||
| class MyThing(lt.Thing): | ||
| my_property: int = lt.property(default=42) | ||
| """A property that holds an integer value.""" | ||
|
|
||
| @lt.property | ||
| def twice_my_property(self) -> int: | ||
| """Twice the value of my_property.""" | ||
| return self.my_property * 2 | ||
|
|
||
| @twice_my_property.setter | ||
| def twice_my_property(self, value: int): | ||
| """Set the value of twice_my_property.""" | ||
| self.my_property = value // 2 | ||
|
|
||
| Adding a setter makes the property read-write (if only a getter is present, it must be read-only). | ||
|
|
||
| It is possible to make a property read-only for clients by setting its ``readonly`` attribute: this has the same behaviour as for data properties. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import labthings_fastapi as lt | ||
|
|
||
| class MyThing(lt.Thing): | ||
| my_property: int = lt.property(default=42) | ||
| """A property that holds an integer value.""" | ||
|
|
||
| @lt.property | ||
| def twice_my_property(self) -> int: | ||
| """Twice the value of my_property.""" | ||
| return self.my_property * 2 | ||
|
|
||
| @twice_my_property.setter | ||
| def twice_my_property(self, value: int): | ||
| """Set the value of twice_my_property.""" | ||
| self.my_property = value // 2 | ||
|
|
||
| # Make the property read-only for clients | ||
| twice_my_property.readonly = True | ||
|
|
||
rwb27 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| In the example above, ``twice_my_property`` may be set by code within ``MyThing`` but cannot be written to via HTTP requests or `.DirectThingClient` instances. | ||
|
|
||
| Functional properties may not be observed, as they are not backed by a simple value. If you need to notify clients when the value changes, you can use a data property that is updated by the functional property. In the example above, ``my_property`` may be observed, while ``twice_my_property`` cannot be observed. It would be possible to observe changes in ``my_property`` and then query ``twice_my_property`` for its new value. | ||
|
|
||
| HTTP interface | ||
| -------------- | ||
|
|
||
| LabThings is primarily controlled using HTTP. Mozilla have a good `Overview of HTTP`_ that is worth a read if you are unfamiliar with the concept of requests, or what ``GET`` and ``PUT`` mean. | ||
|
|
||
| Each property in LabThings will be assigned a URL, which allows it to be read and (optionally) written to. The easiest way to explore this is in the interactive OpenAPI documentation, served by your LabThings server at ``/docs``\ . Properties can be read using a ``GET`` request and written using a ``PUT`` request. | ||
|
|
||
| LabThings follows the `HTTP Protocol Binding`_ from the Web of Things standard. That's quite a detailed document: for a gentle introduction to HTTP and what a request means, see | ||
|
|
||
| .. _`Overview of HTTP`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview | ||
| .. _`HTTP Protocol Binding`: https://w3c.github.io/wot-binding-templates/bindings/protocols/http/index.html | ||
|
|
||
| Observable properties | ||
| ------------------------- | ||
|
|
||
| Properties can be made observable, which means that clients can subscribe to changes in the property's value. This is useful for properties that change frequently, such as sensor readings or instrument settings. In order for a property to be observable, LabThings must know whenever it changes. Currently, this means only data properties can be observed, as functional properties do not have a simple value that can be tracked. | ||
|
|
||
| Properties are currently only observable via websockets: in the future, it may be possible to observe them from other `.Thing` instances or from other parts of the code. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.