Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
Report bugs using our issue tracker.
If you are reporting a bug, please make sure to fill out the appropriate issue template.
Look through the repository issue tracker for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it.
Look through the repository issue tracker for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it.
flatland could always use more documentation, whether as part of the official flatland docs, in docstrings, or even on the web in blog posts, articles, and such. A quick reference for writing good docstrings is available at writing-docstrings.
The best way to send feedback is to file an issue.
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that contributions are welcome :)
Ready to contribute? Here's how to set up flatland
for local development.
-
Make sure all supported Python interpreters (3.10, 3.11, 3.12) are available. This is important because you want to be able to run the test with all supported versions. We recommend pyenv to manage Python versions. See their docs for installation instructions.
-
Open up a terminal, clone the
flatland
repository and open it:git clone git@github.com:flatland-association/flatland-rl.git cd flatland-rl
-
When using
pyenv
, set the local Python versions:pyenv local 3.10 3.11 3.12
-
Set up a virtual environment using your preferred method (we suggest the built-in
venv
) and activate it. You can use your IDE to do this or by using the command line:python -m venv .venv source .venv/bin/activate
-
Install dependencies required for development using pip:
python -m pip install -r requirements-dev.txt python -m pip install -r requirements-ml.txt
-
Create a branch for local development:
git checkout -b name-of-your-bugfix-or-feature
-
Make all the changes you want to make!
-
When you're done making changes, check that your changes pass the tests. Use
tox
to run them as it will automatically test on all supported Python versions:tox
-
If you used a Python package not yet used before in the project, add it to the
dependencies
section of thepyproject.toml
file. Make sure to re-generate the requirement files:tox -e requirements
-
Whenever you feel like you completed an iteration of your changes, commit and push them to GitHub:
git add . git commit # Your favorite editor opens, allowing you to enter a message that describes your changes. The first line is the # subject line. Use sentence capitalisation (but don't end with a period) and limit it to 50 characters. It's good # practice to use imperative mood, e.g. "Add new feature that does X". If you need more space to describe your # changes (focus on the what and why, less on the how), add an empty line and then continue with the body. Try to # limit every line in body to 72 characters. git push --set-upstream origin name-of-your-bugfix-or-feature # After the first time, a simple `git push` will do the trick.
-
Open a pull request on GitHub targeting the
main
branch. Make sure to fill out the template. A review from a core team member is automatically requested. At least one approval is required to merge. -
Once successfully reviewed, squash-merge the PR. This collapses all the commits into one and merges it into the
main
branch. Please adjust the subject line and body of the commit to accurately reflect your changes.
Please adhere to the general Clean Code principles. For instance, we write short and concise functions and use appropriate naming to ensure readability.
We use the pylint naming conventions:
module_name
package_name
ClassName
method_name
ExceptionName
function_name
GLOBAL_CONSTANT_NAME
global_var_name
instance_var_name
function_parameter_name
local_var_name
Docstrings should be formatted using numpydoc.
We use importlib-resources to read from local files.
Sample usages:
from importlib_resources import path
with path(package, resource) as file_in:
new_grid = np.load(file_in)
and:
from importlib_resources import read_binary
load_data = read_binary(package, resource)
self.set_full_state_msg(load_data)
Renders the scene into a image (screenshot):
renderer.gl.save_image("filename.bmp")
We use type hints (PEP 484) for better readability and better IDE support:
# This is how you declare the type of a variable type in Python 3.6
age: int = 1
# You don't need to initialize a variable to annotate it
a: int # Ok (no value at runtime until assigned)
# The latter is useful in conditional branches
child: bool
if age < 18:
child = True
else:
child = False
To get started with type hints, you can have a look at the Type Hints Cheat Sheet.
Caveat: We discourage the usage of type aliases for structured data since its members remain unnamed. Instead, consider
using NamedTuple
:
# Discouraged: Type Alias with unnamed members
Tuple[int, int]
# Better: use NamedTuple
from typing import NamedTuple
Position = NamedTuple('Position', [
('r', int),
('c', int)
])
For structured data containers for which we do not write additional methods, we use NamedTuple
instead of plain Dict
to ensure better readability:
from typing import NamedTuple
RailEnvNextAction = NamedTuple('RailEnvNextAction', [
('action', RailEnvActions),
('next_position', RailEnvGridPos),
('next_direction', Grid4TransitionsEnum)
])
Members of NamedTuple can then be accessed through .<member>
instead of ['<key>']
.
We use classes for data structures if we need to write methods that ensure (class) invariants over multiple members, for
instance, o.A
always changes at the same time as o.B
. We use the attrs
class decorator and a way to declaratively define the attributes on that class:
@attrs
class Replay(object):
position = attrib(type=Tuple[int, int])
We use the abc class decorator and a way to declaratively define the attributes on that class:
# abc_base.py
import abc
class PluginBase(metaclass=abc.ABCMeta):
@abc.abstractmethod
def load(self, input):
"""Retrieve data from the input source and return an object."""
@abc.abstractmethod
def save(self, output, data):
"""Save the data object to the output."""
And then:
# abc_subclass.py
from abc_base import PluginBase
class SubclassImplementation(PluginBase):
def load(self, input):
return input.read()
def save(self, output, data):
return output.write(data)
if __name__ == '__main__':
print('Subclass:', issubclass(SubclassImplementation,
PluginBase))
print('Instance:', isinstance(SubclassImplementation(),
PluginBase))
We discourage currying to encapsulate state since we often want the stateful object to have multiple methods (but the curried function has only its signature, and abusing params to switch behavior is not very readable). Thus, we should refactor our generators and use classes instead.
# Type Alias
RailGeneratorProduct = Tuple[GridTransitionMap, Optional[Dict]]
RailGenerator = Callable[[int, int, int, int], RailGeneratorProduct]
# Currying: a function that returns a confectioned function with internal state
def complex_rail_generator(nr_start_goal=1,
nr_extra=100,
min_dist=20,
max_dist=99999,
seed=1) -> RailGenerator:
To publish a new version of the package:
- Pick an appropriate version number (this project follows semantic versioning, hence chose wisely) and
update
CHANGELOG.md
accordingly. - Create a branch using the naming convention
release/<version-number>
, e.g.release/4.0.0
. - Commit your changes to
CHANGELOG.md
. - Push the branch and open a PR targeting
main
and get it approved by a core team member. - Merge the PR, a GitHub action will pick it up and release the new version.