Skip to content

Commit b6fecd7

Browse files
committed
docs: Add guidelines for public interface management
These guidelines sets out some approaches for managing our public interface, it presents options for integrating code related to breaking changes into the main branch while still allowing control as to when those changes are released to users. Happy for any suggestions, there may be better options or problems with these options that I'm overlooking.
1 parent a563a20 commit b6fecd7

File tree

7 files changed

+270
-0
lines changed

7 files changed

+270
-0
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ For developers
8484
:maxdepth: 1
8585

8686
developers
87+
interface_management
8788
CODE_OF_CONDUCT
8889
docs
8990
persisting_n3_terms

docs/interface_guidelines.rst

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
Public interface guidelines
2+
===========================
3+
4+
Overview
5+
--------
6+
7+
This document provides guidelines for the management of RDFLib's public
8+
interface.
9+
10+
These guidelines attempt to balance the following:
11+
12+
* The need for stable public interface that do not frequently change in ways that are
13+
backwards incompatible.
14+
* The need for modern, easy to use, well-designed public interface free of defects.
15+
* The need to change the public interface in a backwards incompatible way to
16+
modernize it, eliminate defects, and improve usability and design.
17+
18+
Versioning
19+
----------
20+
21+
RDFLib follows semantic versioning [SEMVER]_, which can be summarized as:
22+
23+
Given a version number MAJOR.MINOR.PATCH, increment the:
24+
25+
#. MAJOR version when you make incompatible API changes
26+
#. MINOR version when you add functionality in a backwards compatible
27+
manner
28+
#. PATCH version when you make backwards compatible bug fixes
29+
30+
Additional labels for pre-release and build metadata are available as
31+
extensions to the MAJOR.MINOR.PATCH format.
32+
33+
Guidelines for backwards compatible changes
34+
---------------------------------------------
35+
36+
Examples of backwards compatible changes include:
37+
38+
* Adding entirely new modules, functions, variables, or classes.
39+
* Adding new parameters to existing functions with defaults that maintains the
40+
existing behavior of the function.
41+
42+
Non-breaking changes can be made directly in the main RDFLib module, i.e.
43+
:mod:`rdflib`.
44+
45+
In some cases it may be appropriate to place new identifiers under a
46+
``_provisional`` module (see `Provisional Python modules`_), especially if they
47+
provide complex functionality, this gives the users of RDFLib some opportunity
48+
to try them in non-critical settings while still allowing RDFLib developers to
49+
change the behavior before committing to them and making them part of the public
50+
interface.
51+
52+
Guidelines for backwards incompatible changes
53+
---------------------------------------------
54+
55+
Examples of backwards incompatible changes include:
56+
57+
* Altering a function to behave differently in a way that cannot be considered a
58+
bug fix, some more specific examples:
59+
60+
* Changing the return type to an incompatible type.
61+
* Changing the supported argument types to incompatible types.
62+
* Changing the required arguments.
63+
* Changing the class hierarchy so that class relations that held previously no
64+
longer holds.
65+
66+
When backwards incompatible changes will be introduced users should be provided
67+
with at least one release advanced notice, and, if possible, users should be
68+
given the opportunity to adapt their code to the new behavior before the old
69+
behavior is removed.
70+
71+
At the same time it is preferable to integrate code that implements the new
72+
behavior of backwards incompatible changes into the main branch of RDFLib as
73+
soon as possible, so that long-lived pull requests or branches can be avoided.
74+
75+
This section details various options for integrating code that implements
76+
backwards incompatible changes into the main branch while allowing maintainers
77+
explicit choice in when the backwards incompatible changes are released to users
78+
and allowing users of RDFLib the opportunity to opt into the new behavior.
79+
80+
Simple deprecation
81+
^^^^^^^^^^^^^^^^^^
82+
83+
In the simplest case if a function, class, or variable will be removed or
84+
replaced with a new function with a different name, the old identifier should be
85+
marked as deprecated and the deprecation notice should indicate the new function
86+
that should be used instead.
87+
88+
For functions `warnings.warn` should be used with `DeprecationWarning`, and for
89+
variables and classes the `Sphinx deprecated directive
90+
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-deprecated>`_
91+
should be used.
92+
93+
When deprecating whole classes a deprecation warning should be raised from
94+
`object.__init__` or `object.__new__`.
95+
96+
The deprecation notice should indicate in what version of RDFLib the identifiers
97+
will be removed, and in most cases this should be the next major version.
98+
99+
Release preparation for major versions of RDFLib should remove all code that was
100+
marked as deprecated for removal in the new major version.
101+
102+
Use versioned top level module
103+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
104+
105+
In some cases critical parts of the class hierarchy like `rdflib.graph.Graph`
106+
and `rdflib.term.Node` must be changed. For changes like this versioned top
107+
level modules can be used (see `Top-level Python modules`_).
108+
109+
For example, if a new `rdflib.graph.Graph` class hierarchy needs to be created
110+
for RDFLib 7, the process would be:
111+
112+
* Some time before releasing RDFLib 7:
113+
114+
* Create ``rdflib.v7.graph.Graph``.
115+
* Mark `rdflib.graph.Graph` as deprecated.
116+
* When releasing RDFLib 7:
117+
118+
* Remove `rdflib.graph.Graph`.
119+
* Import ``rdflib.v7.graph.Graph`` to `rdflib.graph.Graph`
120+
121+
With this approach users can start using the new class before RDFLib 7 is
122+
released, and the class does not have to be kept in a separate branch or pull
123+
request until RDFLib 7 is ready for release.
124+
125+
Some care must be taken to avoid duplicated code, some options available to do
126+
this is:
127+
128+
* Move shared functionality out to another class or function that can be used
129+
from the old and new classes.
130+
* Import the old class and override the needed/relevant functionality.
131+
132+
Release preparation for major versions should import the identifiers from the
133+
versioned python modules (e.g. :mod:`rdflib.v7`) into the main python module
134+
(i.e. :mod:`rdflib`) and remove the identifiers that were marked as deprecated.
135+
136+
Use the ``rdflib._version._ENABLE_V{major_version}`` flags
137+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
138+
139+
With each released of RDFLib the corresponding
140+
``rdflib._version._ENABLE_V{major_version}`` variable will be set to `True`.
141+
142+
These variables can be used to select default values or change other behavior of
143+
functions, for example changing the default format for `rdflib.graph.Graph.serialize`.
144+
145+
Boolean flags are used as they can be used with the ``always_true`` and
146+
``always_false`` `directives from mypy
147+
<https://mypy.readthedocs.io/en/stable/config_file.html#confval-always_true>`_.
148+
149+
Python modules
150+
--------------
151+
152+
This section describes Python modules that are relevant to the management of
153+
RDFLib's public interface.
154+
155+
Top-level Python modules
156+
^^^^^^^^^^^^^^^^^^^^^^^^
157+
158+
:mod:`rdflib`, the main Python module:
159+
This is the main Python module for RDFLib and represents the public
160+
interface of the current version of RDFLib.
161+
162+
``rdflib{major_version}`` (e.g. :mod:`rdflib.v7`), versioned modules:
163+
These modules are for parts of RDFLib public interface specific to a major
164+
version, and is intended to be imported into the main RDFLib module (i.e.
165+
:mod:`rdflib`) for RDFLib releases with the same or later major version.
166+
167+
So for example, identifiers from the :mod:`rdflib.v7` module should not be
168+
imported as public identifiers into :mod:`rdflib` until version 7 of RDFLib
169+
is released but may be symlinked into :mod:`rdflib` from version 7 and
170+
onward.
171+
172+
These modules exist to facilitate integrating code into the main branch that
173+
should go into future major versions of RDFLib, and in most cases will be
174+
used for things that replace parts of the older interface.
175+
176+
Versioned modules part of the public RDFLib interface, and once something
177+
appears in a versioned module it should remain part of the public interface
178+
until the next major version of RDFLib.
179+
180+
That is, things from :mod:`rdflib.v7` should not be removed until version 8 of
181+
RDFLib is released.
182+
183+
184+
Provisional Python modules
185+
^^^^^^^^^^^^^^^^^^^^^^^^^^
186+
187+
``rdflib._provisional`` and ``rdflib.v{major_version}._provisional`` (e.g. ``rdflib.v7._provisional``), provisional modules:
188+
These modules contain code that is expected to become part of :mod:`rdflib`
189+
and ``rdflib.v{major_version}`` respectively but may change before this
190+
happens.
191+
192+
Given that the provisional modules are marked for internal use (i.e.
193+
prefixed with an underscore ``_``) they are not considered part of RDFLib's
194+
public interface and therefore the guarantees associated with RDFLib's
195+
public interface does not extend to content of these modules, however, there
196+
is some expectation that the provisional module will be used and to minimize
197+
the impact to users of the provisional module anything that is moved out of
198+
the provisional module into the main or versioned RDFLib modules will be
199+
imported back into the provisional module so that code that used the
200+
provisional interface can potentially still remain operational.
201+
202+
Practical Examples
203+
------------------
204+
205+
Glossary
206+
--------
207+
208+
`Python identifier <https://docs.python.org/3/reference/lexical_analysis.html#identifiers>`_
209+
The name of a module, variable, class, function, or function argument. In
210+
some cases this is also referred to as a Python name.
211+
212+
Backwards incompatible change
213+
A change to RDFLib that require changes to code that use RDFLib in order for
214+
that code to keep performing the same function. Also referred to as a
215+
breaking change.
216+
217+
`Public interface <https://en.wikipedia.org/wiki/Public_interface>`_
218+
The parts of RDFLib that are intended for use by the users of RDFLib. This
219+
more or less corresponds to the identifiers that are not directly or
220+
indirectly marked as internal by prefixing them or packages containing them
221+
with a single leading underscore [PEP8]_.
222+
223+
Public Python identifier
224+
A Python identifier that qualifies as part of a public interface.
225+
226+
`Python module <https://docs.python.org/3/glossary.html#term-module>`_
227+
"An object that serves as an organizational unit of Python code. Modules
228+
have a namespace containing arbitrary Python objects. Modules are loaded
229+
into Python by the process of importing."
230+
231+
The phrase "Python module" is used preferentially over `Python package
232+
<https://docs.python.org/3/glossary.html#term-package>`_ as it is less
233+
ambiguous.
234+
235+
References
236+
----------
237+
238+
.. [PEP8] `PEP 8 – Style Guide for Python Code
239+
<https://peps.python.org/pep-0008/>`_
240+
.. [SEMVER] `Semantic Versioning 2.0.0 <https://semver.org/spec/v2.0.0.html>`_

rdflib/_provisional/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
This module contains code that may become part of :mod:`rdflib` though is
3+
expected to change further before that happens. Once code here becomes part of
4+
:mod:`rdflib` it should be imported back into :mod:`rdflib._provisional` so that
5+
code that used :mod:`rdflib._provisional` can continue working.
6+
"""

rdflib/_version.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
This module contains information about the version of RDFLib.
3+
"""
4+
5+
6+
# _ENABLE_V? flags are used to control version specific behaviour
7+
_ENABLE_V6 = True
8+
_ENABLE_V7 = False
9+
_ENABLE_V8 = False
10+
_ENABLE_V9 = False

rdflib/v7/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""
2+
This module contains code that will become part of :mod:`rdflib` once RDFLib 7
3+
is released.
4+
"""

rdflib/v7/_provisional/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
This module contains code that may become part of :mod:`rdflib.v7` though is
3+
expected to change further before that happens. Once code here becomes part of
4+
:mod:`rdflib.v7` it should be imported back into :mod:`rdflib.v7._provisional`
5+
so that code that used :mod:`rdflib.v7._provisional` can continue working.
6+
"""

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ disallow_subclassing_any = False
5858
warn_unreachable = True
5959
warn_unused_ignores = True
6060

61+
always_true = rdflib._version._ENABLE_V6
62+
always_false = rdflib._version._ENABLE_V7,rdflib._version._ENABLE_V8,rdflib._version._ENABLE_V9
63+
6164
# This is here to exclude the setup.py files in test plugins because these
6265
# confuse mypy as mypy think they are the same module.
6366
exclude = (?x)(

0 commit comments

Comments
 (0)