Skip to content

Commit 13a4dff

Browse files
authored
Merge pull request #162 from kneasle/better-contributing
Improve info for contributors
2 parents 5eb2447 + 974ded8 commit 13a4dff

File tree

2 files changed

+203
-54
lines changed

2 files changed

+203
-54
lines changed

ARCHITECTURE.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Architecture
2+
3+
This file provides a high-level overview of the code, inspired by
4+
[matklad's blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html) of the same name.
5+
The purpose of this file is to provide a convenient 'mental map' of Wheatley's code to help new
6+
contributors familiarise themselves with the code base. It aims to answers general questions like
7+
"where do I find code to do X?" or "what does the code in Y do?", but doesn't go into great detail
8+
about the inner workings of each section.
9+
10+
## Bird's Eye View of the Problem
11+
12+
Wheatley is a 'bot' ringer for the Ringing Room platform. Wheatley also adapts to the actions of
13+
other human ringers _in real time_ rather than simply expecting them to keep up with a set pace.
14+
To achieve this goal, Wheatley has three tasks once the start-up is complete:
15+
16+
1. Know what is being rung (i.e. what order are the bells expected to ring). This can be affected
17+
dynamically by human ringers making calls like `Bob`, `Single`, `Go`, etc.
18+
2. Listen for human-controlled bells being rung, and use this new information to update some
19+
internal sense of 'rhythm'.
20+
3. Combine, in real time, the expected order of the bells (from 1.) with the real-time data from
21+
human ringers (from 2.) in order to ring the Wheatley-controlled bells at the 'right' times.
22+
23+
## Code Map
24+
25+
The bulk of the code resides in the `wheatley/` directory (which the only code is shipped to users).
26+
However, there are some other pieces of code useful during development which reside in the main
27+
directory:
28+
29+
- `run-wheatley`: An executable Python script which will run the Wheatley code directly as though it
30+
were run through `pip`.
31+
- `tests/*`: Unit tests for various pieces of Wheatley's code. This only tests code in the
32+
`wheatley/` folder.
33+
- `doctests`: Python script which invokes all the examples found in `README.md`, asserting that they
34+
don't crash. This prevents the examples from getting out of sync with the code.
35+
- `fuzz`, `fuzzing/*`: Fuzzing for CLI argument parsers. These feed the parsing fuctions with
36+
thousands of randomly generated inputs, asserting that they must produce well-defined errors.
37+
38+
### `wheatley/{aliases.py, bell.py, calls.py, stroke.py}`
39+
40+
These files contain little or no business logic, and instead provide datatypes (`bell.py`,
41+
`stroke.py`), type aliases (`aliases.py`) and/or constants that are used extensively throughout the
42+
code. These serve two purposes:
43+
1. They increase safety by providing an abstraction layer over the raw numbers and strings used to
44+
communicate with Ringing Room.
45+
2. `bell.py` prevents any ambiguity between the many different representations of bell names (i.e.
46+
is the 12th called `'T'` (name), `12` (number) or `11` (index)?) - Wheatley uses one `Bell` class
47+
which provides unambiguous conversions to and from these representations.
48+
49+
### `wheatley/bot.py`
50+
51+
This is the glue code that holds Wheatley together. The `Bot` class is a singleton that gets
52+
created at start-up and mediates interactions between other parts of the code (row generation,
53+
rhythm, interacting with Ringing Room, etc). It also runs the `mainloop` - an infinite loop in
54+
which the main thread gets stuck until Wheatley stops.
55+
56+
**Architectural Invariant**: None of the code in `row_generation/*`, `rhythm/*` or `tower.py` can
57+
talk directly to each other; instead they all provide an interface that the `Bot` class to mediate
58+
interactions.
59+
60+
### `wheatley/rhythm/*`
61+
62+
This is where the rhythm detection happens. The specification of a rhythm is defined in
63+
`abstract_rhythm.py`, and there are two `Rhythm` classes which implement different behaviours:
64+
- `regression.py` uses regression to draw a linear line of best fit through the user's datapoints
65+
and then uses this line to decide where Wheatley's bell should ring.
66+
- `wait_for_user.py` adds waiting for user-controlled bells on top of an existing `Rhythm` class.
67+
68+
**Architectural Invariant**: The rhythm module should never access the real time directly - it
69+
should use the times passed to each individual method. This is because `WaitForUserRhythm` lies to
70+
its internal rhythm in order to stop Wheatley from jumping back to the original rhythm if someone
71+
holds up for a long time.
72+
73+
### `wheatley/row_generation/*`
74+
75+
This is the code that determines _what_ Wheatley rings, and reacts to the calls `Bob` and `Single`.
76+
`row_generator.py` specifies the interface of a `RowGenerator`, and each different source of rows
77+
(method, CompLib, dixonoid, etc.) has its own file and class.
78+
79+
**Architectural Invariant**: The `row_generation` module does _not_ handle adding cover bells or
80+
responding to calls like `Go`, `That's All`, `Stand`. These are both handled by the `Bot` class,
81+
since both cover bells and state transitions are indepedent of what row generator is being used.
82+
83+
### `wheatley/{main.py, parsing.py}`
84+
85+
This is the start-up code for Wheatley. It gets called once and is tasked with parsing the user's
86+
input and then using this to generate `Rhythm`, `RowGenerator`, `Tower` and `Bot` singletons.
87+
Finally, it enters the `Bot`'s mainloop, which never returns.
88+
89+
`parsing.py` also contains some code for interpreting the SocketIO signals which change the controls
90+
in the integrated version, but this will likely be moved somewhere else.
91+
92+
Wheatley has 3 main functions:
93+
- `server_main`: The integrated Ringing Room version's main function
94+
- `console_main`: The CLI version's main function
95+
- `main`: The root main function, which delegates to one of the other two main functions depending
96+
on whether or not Wheatley is running on a Ringing Room server
97+
98+
**Architectural Invariant**: This is the only place where different code is executed between the
99+
CLI and integrated versions. 90% of the differences between versions are implemented by
100+
disconnecting callbacks during initialisation.
101+
102+
### `wheatley/{tower.py, page_parsing.py}`
103+
104+
**NOTE: This code is soon going to be replaced with
105+
[belltower](https://github.com/kneasle/belltower), which can be used for other projects.**
106+
107+
These files handle all the direct contact with Ringing Room, and provide an abstraction barrier
108+
between the rest of the code and the internal workings of Ringing Room. The `Tower` class in
109+
`tower.py` handles run-time connections to Ringing Room, whereas `page_parsing.py` is used during
110+
start-up to parse information out of the HTML source of the Ringing Room pages.
111+
112+
This abstraction layer means two things:
113+
1. 90% of Wheatley is completely platform indepedent - supporting a new platform (other than Ringing
114+
Room) would only require creating a new `Tower` class.
115+
2. If Ringing Room changes its API in any way, the corresponding changes to Wheatley are limited to
116+
just these files.
117+
118+
**Architectural Invariant**: Every interaction with Ringing Room is handled through these modules -
119+
the rest of the code does't even know it's talking to Ringing Room.

CONTRIBUTING.md

Lines changed: 84 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,92 @@
11
# Contributing
2-
Pull requests are welcome, but please
3-
[make an issue](https://github.com/Kneasle/ringing-room-bot/issues/new) to discuss the changes
4-
before starting to implement things.
52

6-
To run Wheatley **from source code**, `cd` to the repository directory and run:
3+
**First**: if you're unsure or afraid of _anything_, just ask or submit the issue or pull request
4+
anyway. You won't be yelled at for giving it your best effort. The worst that can happen is that
5+
you'll be politely asked to change something. We appreciate any sort of contributions, and don't
6+
want a wall of rules to get in the way of that.
7+
8+
If you're new to the code, a high-level description of Wheatley's internal architecture can be found
9+
in [ARCHITECTURE.md](https://github.com/kneasle/wheatley/blob/better-contributing/ARCHITECTURE.md).
10+
11+
## Getting Started
12+
13+
- Fork the [kneasle/wheatley](https://github.com/kneasle/wheatley/pull/) repository and use `git
14+
clone` to create a local 'cloned' copy on your machine
15+
- (optional) Create and activate a
16+
[virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/)
17+
to keep Wheatley's dependencies separate system-wide packages.
18+
- Install python dependencies with `pip install -r requirements.txt`. This will also install the
19+
dependencies required to run the Pull Request checks locally.
20+
- Do your magic and make the changes you want to your local code.
21+
22+
## Running Your Modified Code
23+
24+
To run Wheatley **from source code**, `cd` to the repository root and run:
25+
726
```bash
27+
python run-wheatley [ARGS]
28+
# or possibly
829
python3 run-wheatley [ARGS]
930
```
10-
(or `python run-wheatley [ARGS]` on Windows).
1131

12-
Or, on Unix you can run `./run-wheatley [ARGS]`.
32+
Or, on Unix you can run `./run-wheatley [ARGS]`. Basically, replacing `wheatley` with
33+
`python run-wheatley` or `./run-wheatley` will have exactly the same effect but will use your
34+
updated source code rather than the version installed by `pip`.
35+
36+
## PR checks
37+
38+
In order to make the code more reliable, all incoming PRs are passed through several checks which
39+
automatically look out for and catch mistakes. All of these can be run with the following:
40+
41+
```bash
42+
python -m mypy wheatley --pretty --disallow-incomplete-defs --disallow-untyped-defs
43+
python -m pylint wheatley
44+
python -m pytest
45+
./doctests
46+
./fuzz
47+
```
48+
49+
### Typechecker
50+
51+
These checks require that, on all incoming PRs, the entire codebase has
52+
[type annotations](https://docs.python.org/3/library/typing.html), and that these type annotations
53+
are consistent (e.g. you're not passing an `int` into a function that expects a `str`). Therefore,
54+
any new or modified code has to continue satisfying the type checker in order for a PR to be merged.
55+
The typechecker can be run locally with the following command and will produce reasonably good error
56+
messages if anything is wrong:
57+
58+
```bash
59+
python -m mypy wheatley --pretty --disallow-incomplete-defs --disallow-untyped-defs
60+
```
61+
62+
### Linting
1363

14-
## Code structure
64+
All PRs are also checked by the linter `pylint`, which checks the code automatically for small
65+
issues. It can be run locally with:
66+
67+
```bash
68+
python -m pylint wheatley
69+
```
70+
71+
### Unit Tests, Doctests and Fuzzing
72+
73+
These last checks are tests that are built into the source code, and fall into three categories:
74+
75+
1. **Unit Tests**: Small functions which test individual 'units' of the code, e.g. parsing CLI
76+
arguments. These make sure that these components do what is expected, and that this behaviour
77+
doesn't subsequently change.
78+
2. **Doctests**: Doctests are short for 'documentation tests'. In this case, the documentation in
79+
question are the examples in the `README`, and this runs all the examples and asserts that they
80+
all enter `Bot`'s mainloop correctly. This forces us to keep the examples up to date with any
81+
changes to the code.
82+
3. **Fuzzing**: Fuzzing is the process of providing some function with large amounts of random input
83+
and asserting that it doesn't behave unexpectedly. In our case, we fuzz the CLI arg parsing code
84+
to make sure that Wheatley will produce pleasant error messages regardless of the user enters.
85+
86+
All three of these can be run with:
87+
88+
```bash
89+
python -m pytest
90+
./doctests
91+
./fuzz
1592
```
16-
.
17-
├── CHANGE_LOG.md
18-
├── CONTRIBUTING.md
19-
├── LICENSE
20-
├── README.md
21-
├── requirements.txt
22-
├── run-wheatley => A shell-executable python script to launch Wheatley
23-
├── setup.py => Build script run to generate the PIP package
24-
├── wheatley
25-
│   ├── __init__.py => Empty file
26-
│   ├── __main__.py => Code to start Wheatley
27-
│ ├── arg_parsing.py => Code to parse formatted CLI args e.g. call strings
28-
│   ├── bell.py => Stores the representation of a bell
29-
│   ├── bot.py => The main class for Wheatley
30-
│   ├── calls.py => Constants for the calls used in Ringing Room
31-
│   ├── main.py => Entry point of Wheatley
32-
│   ├── page_parser.py => Code to parse the ringing room HTML
33-
│   ├── regression.py => Code for the weighted linear regression
34-
│   ├── rhythm.py => Code used to drive Wheatley's rhythm
35-
│   ├── tower.py => Code used to interact with ringing room
36-
│   └── row_generation => Modular row generators
37-
│    ├── __init__.py
38-
│    ├── complib_composition_generator.py
39-
│    ├── dixonoids_generator.py
40-
│     ├── go_and_stop_calling_generator.py
41-
│    ├── helpers.py
42-
│     ├── method_place_notation_generator.py
43-
│    ├── place_notation_generator.py
44-
│    ├── plain_hunt_generator.py
45-
│    └── row_generator.py
46-
├── tests
47-
│   ├── __init__.py
48-
│   ├── test_arg_parsing.py => Unit tests for CLI arg parsing
49-
│   └── row_generation => Unit tests for the row generation module
50-
│      ├── __init__.py
51-
│      ├── generator_test_helpers.py
52-
│      ├── test_DixonoidsGenerator.py
53-
│      ├── test_Helpers.py
54-
│      ├── test_MethodPlaceNotationGenerator.py
55-
│      ├── test_PlaceNotationGenerator.py
56-
│      └── test_PlainHuntGenerator.py
57-
├── fuzz => Shell-executable script to run all the fuzzing
58-
└── fuzzing
59-
   ├── __init__.py
60-
   ├── call_parsing.py
61-
   └── fuzz_utils.py
62-
```

0 commit comments

Comments
 (0)