Skip to content

Commit

Permalink
Merge branch 'main' of github.com:henryiii/se-for-sci
Browse files Browse the repository at this point in the history
  • Loading branch information
rteyssier committed Sep 6, 2024
2 parents 5a89366 + c04aef5 commit 2444c1e
Show file tree
Hide file tree
Showing 20 changed files with 656 additions and 308 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v2

- name: Build the Pyodide output
run: |
pipx run nox -s pyodide
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,5 @@ a.out
compile_flags.txt
Untitled*
content/week12/06-rust/Cargo.lock

_output/
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@ repos:
- id: requirements-txt-fixer
- id: trailing-whitespace

- repo: https://github.com/psf/black
rev: 24.8.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.3
hooks:
- id: black-jupyter
- id: ruff-format
exclude: ^content/week01/cleanup_bessel\.ipynb$

- repo: https://github.com/asottile/blacken-docs
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.18.0
hooks:
- id: blacken-docs
additional_dependencies: [black==23.*]
additional_dependencies: [black==24.*]

- repo: https://github.com/kynan/nbstripout
rev: 0.7.1
hooks:
- id: nbstripout

- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v4.0.0-alpha.8"
- repo: https://github.com/rbubley/mirrors-prettier
rev: "v3.3.3"
hooks:
- id: prettier

Expand Down
169 changes: 120 additions & 49 deletions content/week01/practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ differences as meaningful, adding cognitive load to future readers (including
you). Avoid it!

For Python, style is described in PEP 8, and the most popular autoformatter is
Black. I'd highly recommend sticking to that style if you don't have strong
Black (or Ruff, which has a similar style but is faster, as it's written in
Rust). I'd highly recommend sticking to that style if you don't have strong
preferences otherwise. For C++, there are more to choose from - pick one and be
consistent. LLVM's styling is good. Again, styling can be enforced by tools like
clang-format. We'll cover those sorts of things later.
Expand All @@ -129,13 +130,127 @@ Notice the style:
- Four space indent
- Power operator `**` doesn't need spaces for simple expressions

### The function signature as a contract

A function's signature should be a contract between the function implementer
(you) and the function user (might also be you). Something like this:

```
output1, output2, ... = function(input1, input2, ...)
```

This is not always possible though, depending on the language and situation. The
things to avoid, and some reasons you can't in some situations:

#### Argument mutation

You generally should not mutate an argument. This can even sneak in when you are
writing Python where you don't expect it. Take the following function:

```python
def add_end_to_list(x=[]):
x.append("end")
return x
```

This might do what you expect at first:

```python
my_list = ["start"]
print(add_to_list(my_list))
```

But check the contents of `my_list` afterwards. Even better, try running it with
the default argument (`add_to_list()`) and see what it returns.

Due to the above, it's a convention in Python to never use a mutable structure
(we'll discuss mutation in detail in a few weeks) like a list or a dict for an
argument default. Unless you really have to, you should also avoid mutating an
input argument. Here is a better version of the above function:

```python
def add_end_to_list(x=()):
return [*x, "end"]
```

If you do need to mutate arguments, it should be well documented and clear as
possible from the function and argument names. Usually you should not return the
list

```python
def append_end_to_list(x=None):
x_list = x or []
x_list.append("end")
```

Now, since it doesn't return anything, a user is more likely to be aware that
it's mutating the input. They are much less likely to assume the output is an
independent variable (since there is no usable output, it's just None).

Also, by the way, the default version of this function no longer even makes
sense, since you don't have access to it after the function runs; this further
reduces the potential for mistakes and confusion.

```python
def append_end_to_list(x):
x.append("end")
```

#### Multiple outputs

If your language supports it (C++11 partially, C++17, Rust, Python, etc), then
use multiple outputs over mutating inputs. Some languages (C) do not support
multiple outputs, so the only option for those languages is to ask a user to
make an empty variable and then fill it via passing it as an argument.

#### Outer-scope capture

Python has automatic variable capture[^1] from outer scope. In C++ lambda
functions, you have to explicitly list variables you want to capture, but Python
hides this. This makes this a common source of errors and makes reading the code
much harder! There are a few rare cases where you do need this, but it should be
reserved for functions with short bodies and written in such a way to make it
obvious you need capture. And also consider `functools.partial`, which not only
advertises the intent to capture to the reader, but actually captures the value
when it is created, rather than when it is called later.

```python
# Bad
x = 2


def f():
print(x)


x = 3
```

```python
# Better
x = 2


def f_needs_x(x_value):
print(x_value)


f = functools.partial(f_needs_x, x)
x = 3
```

Remember, the signature of a function is not just for Python, it's telling the
reader what the function expects and what it returns. Capture causes the
function to lie to the reader about what it expects and/or what it changes.

### Avoid a bajillion parameters/function arguments

What do you think of this code?

```python
def simulate_plasma(x_i, v_i, t_i, t_f, E_i, B_i, N, result_array, printflag):
... # do some stuff
def simulate_plasma(
x_i, v_i, t_i, t_f, E_i, B_i, N, result_array, printflag
): ... # do some stuff
```

This has a lot of parameters, making it hard to use / easy to misuse. In Python,
Expand All @@ -150,8 +265,7 @@ Some helpful hints:
Here's an example of bundling. Let's take a simpler example:

```python
def get_rect_area(x1, y1, x2, y2):
... # does stuff
def get_rect_area(x1, y1, x2, y2): ... # does stuff


get_rect_area(x1, y1, x2, y2)
Expand All @@ -161,8 +275,7 @@ Someone calling the function could easily make a mistake:
`get_rect_area(x1, x2, y1, y1)` for example. However, if you bundle this:

```python
def get_rect_area(point_1, point_2):
... # does stuff
def get_rect_area(point_1, point_2): ... # does stuff


get_rect_area(Point(x1, y1), Point(x2, y2))
Expand Down Expand Up @@ -288,48 +401,6 @@ There are some exceptions to this, but it's a decent rule of thumb.
Global **constants** are generally ok. For instance, you probably want to define
`PI` once and let all your code reference it (that's more DRY).

You can take this one step further:

### Minimize capture

Python has automatic variable capture[^1] from outer scope. In C++ lambda
functions, you have to explicitly list variables you want to capture, but Python
hides this. This makes this a common source of errors and makes reading the code
much harder! There are a few rare cases where you do need this, but it should be
reserved for functions with short bodies and written in such a way to make it
obvious you need capture. And also consider `functools.partial`, which not only
advertises the intent to capture to the reader, but actually captures the value
when it is created, rather than when it is called later.

```python
# Bad
x = 2


def f():
print(x)


x = 3
```

```python
# Better
x = 2


def f_needs_x(x_value):
print(x_value)


f = functools.partial(f_needs_x, x)
x = 3
```

Remember, the signature of a function is not just for Python, it's telling the
reader what the function expects and what it returns. Capture causes the
function to lie to the reader.

### Guard pattern

There's a rule in some style checkers that states the following:
Expand Down
2 changes: 1 addition & 1 deletion content/week01/programming_basics.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"\n",
"Just to keep the computational model straight, we are using:\n",
"\n",
"* Python 3.10, a interpreted programming language\n",
"* Python 3.12 (3.10+), a interpreted programming language\n",
"* A REPL, which **R**eads a line, **E**valuates it, then **P**rints it (in a **L**oop)\n",
" - `print` not needed to see the last un-captured value in a cell\n",
"* IPython, which is an enhancement to make Python more **I**nteractive\n",
Expand Down
53 changes: 27 additions & 26 deletions content/week02/advanced_steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,8 @@ $ cat file5.txt
I have changed file5.txt
```

When using `git pull`, you are in fact merging the remote branch with your local branch, using under the hood `git merge`.
When using `git pull`, you are in fact merging the remote branch with your local
branch, using under the hood `git merge`.

## Using GitHub as origin repository

Expand All @@ -529,11 +530,10 @@ You can now push to the remote GitHub repository all your ongoing work using
$ git push --set-upstream origin master
```

If you now look at the GitHub website, you can see all your hard work
listed there, including all the past history. Note that what we just
did never occurs in practice. You will always create an empty project
on GitHub or BitBucket first, and then clone it to your computer and
start editing files there.
If you now look at the GitHub website, you can see all your hard work listed
there, including all the past history. Note that what we just did never occurs
in practice. You will always create an empty project on GitHub or BitBucket
first, and then clone it to your computer and start editing files there.

Let's now learn how to change a file in your local repository and push it to the
remote repository.
Expand Down Expand Up @@ -563,39 +563,40 @@ your code. You are in business!

## Using git rebase instead of git merge

In large projects with multiple developers, merging different branches can
result in very cumbersome histories with multiple diverging tracks converging
back to the master branch with complex patterns.
In large projects with multiple developers, merging different branches can
result in very cumbersome histories with multiple diverging tracks converging
back to the master branch with complex patterns.

`git` offers the `rebase` functionality to combine two branches along the same trunk,
one entire diverging branch after the other. Let's try an example.
Go back to the repository `mywork`. Checkout branch `better_code` and create a new file called `file6.txt`.
Don't forget to `git add` and `git commit`. Then checkout branch `master` and create a new
file called `file7.txt`. Again, `git add` and `git commit`.
`git` offers the `rebase` functionality to combine two branches along the same
trunk, one entire diverging branch after the other. Let's try an example. Go
back to the repository `mywork`. Checkout branch `better_code` and create a new
file called `file6.txt`. Don't forget to `git add` and `git commit`. Then
checkout branch `master` and create a new file called `file7.txt`. Again,
`git add` and `git commit`.

If you `git log`, you will see now 2 diverging branches like before.
This time, we will combine these 2 branches using
If you `git log`, you will see now 2 diverging branches like before. This time,
we will combine these 2 branches using

```console
$ git rebase better_code
Successfully rebased and updated refs/heads/master.
```

If now you type `git log`, you see that the `master` branch has merged the `better_code` branch
in a single timeline. You don't see any diverging and converging path anymore.
Commits made in parallel in the `master` branch appear now after the `better_code` commits.
If now you type `git log`, you see that the `master` branch has merged the
`better_code` branch in a single timeline. You don't see any diverging and
converging path anymore. Commits made in parallel in the `master` branch appear
now after the `better_code` commits.

Use this online tool: https://learngitbranching.js.org/?NODEMO to build examples that highlight the difference between `merge` and `rebase`.

## Example of a complex git repository

Let's now navigate to the BitBucket page of a large collaborative project I
have contributed to, namely the
[RAMSES code](https://bitbucket.org/rteyssie/ramses). You can explore the
different rubrics there, including an automatic test page (more later on this
topic in the course) and a wiki with all the documentation. You can clone the
corresponding repository on your laptop and have fun running hydrodynamics
simulations.
Let's now navigate to the BitBucket page of a large collaborative project I have
contributed to, namely the [RAMSES code](https://bitbucket.org/rteyssie/ramses).
You can explore the different rubrics there, including an automatic test page
(more later on this topic in the course) and a wiki with all the documentation.
You can clone the corresponding repository on your laptop and have fun running
hydrodynamics simulations.

## See also

Expand Down
1 change: 1 addition & 0 deletions content/week02/first_steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,4 @@ Previous HEAD position was 476b980 Commit changes
Switched to branch 'master'
$ ls
file1.txt file2.txt file3.txt
```
1 change: 0 additions & 1 deletion content/week02/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,3 @@ Other resources for git:
- https://ohshitgit.com/
- http://gitready.com/
- https://explainshell.com/

3 changes: 1 addition & 2 deletions content/week03/pytest.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,7 @@ def platform(request, monkeypatch):
return request.param


def test_some_function_linux(monkeypatch, platform):
...
def test_some_function_linux(monkeypatch, platform): ...
```

Now we automatically get the monkeypatching each time, too!
Expand Down
6 changes: 2 additions & 4 deletions content/week05/task_runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,13 @@ You can parametrize sessions by any item, for example Python version.
```python
# Shortcut to parametrize Python
@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12"])
def my_session(session: nox.Session) -> None:
...
def my_session(session: nox.Session) -> None: ...


# General parametrization
@nox.session
@nox.parametrize("letter", ["a", "b"], ids=["a", "b"])
def my_session(session: nox.Session, letter: str) -> None:
...
def my_session(session: nox.Session, letter: str) -> None: ...
```

The optional `ids=` parameter can give the parametrization nice names, like in
Expand Down
Loading

0 comments on commit 2444c1e

Please sign in to comment.