Skip to content
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

SDC Tools sprints 3 and 4 #1276

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d848b5e
SDC Tools sprints 3 and 4
illicitonion Dec 24, 2024
3bd13ce
Add module success criteria
illicitonion Dec 30, 2024
f486b51
Simplify themes
illicitonion Dec 30, 2024
02dd34b
Update common-content/en/module/tools/comparing-javascript-and-python…
illicitonion Jan 22, 2025
ed11da7
Update common-content/en/module/tools/comparing-javascript-and-python…
illicitonion Jan 22, 2025
907eb75
Update common-content/en/module/tools/comparing-javascript-and-python…
illicitonion Jan 22, 2025
0d707cd
Update common-content/en/module/tools/comparing-javascript-and-python…
illicitonion Jan 22, 2025
c60ab8a
Update common-content/en/module/tools/comparing-javascript-and-python…
illicitonion Jan 22, 2025
57d76b7
Update common-content/en/module/tools/comparing-javascript-and-python…
illicitonion Jan 22, 2025
3102d28
Update common-content/en/module/tools/converted-program/index.md
illicitonion Jan 22, 2025
367024b
Update common-content/en/module/tools/converted-program/index.md
illicitonion Jan 22, 2025
038b493
Update common-content/en/module/tools/converting-javascript-to-python…
illicitonion Jan 22, 2025
fec88e1
Update common-content/en/module/tools/counting-words/index.md
illicitonion Jan 22, 2025
9319442
Update common-content/en/module/tools/first-nodejs-program/index.md
illicitonion Jan 22, 2025
3012660
Update common-content/en/module/tools/first-nodejs-program/index.md
illicitonion Jan 22, 2025
516de47
Update common-content/en/module/tools/single-use-data-analysis/index.md
illicitonion Jan 22, 2025
b0493ac
Update common-content/en/module/tools/using-python-dependencies/index.md
illicitonion Jan 22, 2025
03005f1
Update common-content/en/module/tools/using-python-dependencies/index.md
illicitonion Jan 22, 2025
edee0a9
Update common-content/en/module/tools/using-python-dependencies/index.md
illicitonion Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
+++
title = "Comparing JavaScript and Python"
headless = true
time = 40
facilitation = false
emoji= "📖"
objectives = [
"Identify and explain equivalences between JavaScript and Python",
"Compare and contrast differences between JavaScript and Python",
"Distinguish between essential and accidental complexity"
]
+++

JavaScript and Python have many things in common.

Most differences are "cosmetic". Here are some examples of cosmetic differnces:
* Some functions and operators have different names. But often there are functions/operators which do exactly the same thing.
* JavaScript uses `{}` around blocks of code and we _choose_ if we indent code. Python uses `:` and indentation is required.
* In JavaScript we choose to name variables in `camelCase`, whereas in Python we choose to name variables in `snake_case` (but in both langues we _could_ do either).
illicitonion marked this conversation as resolved.
Show resolved Hide resolved

Recall our "count containing words" JavaScript code. Now think about what it would look like in Python.

```js
import { program } from "commander";
import { promises as fs } from "node:fs";
import process from "node:process";

program
.name("count-containing-words")
.description("Counts words in a file that contain a particular character")
.option("-c, --char <char>", "The character to search for", "-");

program.parse();

const argv = program.args;
if (argv.length != 1) {
console.error(`Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`);
process.exit(1);
}
const path = argv[0];
const char = program.opts().char;

const content = await fs.readFile(path, "utf-8");
const wordsContainingChar = content.split(" ").filter((word) => word.indexOf(char) > -1).length;
console.log(wordsContainingChar);
```

Let's think about what we're doing in this code. We're:
illicitonion marked this conversation as resolved.
Show resolved Hide resolved
* Parsing command line flags - writing down what flags we expect to be passed, and reading values for them based on the actual command line.
illicitonion marked this conversation as resolved.
Show resolved Hide resolved
* Validating the flags (i.e. checking that exactly one path was passed).
* Reading a file.
* Splitting the content of the file up into words.
* Counting how many of the words contained a particular character.
* Printing the count.

These are the meaningful things we needed to do. To solve the same problem with Python, we'd still do all of these things.

There are also some other things we did in our code, which were important, but not the point of the code. An example is, we imported some modules. We may need to import modules to write this code in Python. Or we may not. Importing modules wasn't one of our _goals_, it was just something we needed to do to help us.
illicitonion marked this conversation as resolved.
Show resolved Hide resolved

We split up things we need to do into two categories: essential and accidental.

**Essential** means it is a core part of the problem. e.g. in order to count how many words are in a file, it is _essential_ that we read the file.

**Accidental** means it isn't what we _care_ about doing, but we may need to do it anyway. e.g. importing the `process` module isn't _essential_ to our problem, but we needed to do it anyway so we could report errors.

When we're thinking about how we use different languages, it's useful to think about what parts of our problem are _essential_ (we'll need to do them in any language), and which parts are _accidental_ (it's just something we had to do on the way to achieve our aim).

Whether we write the JavaScript `someArray.length` or the Python `len(some_array)` isn't a big difference - both do the same thing, they just look a little a little different.
illicitonion marked this conversation as resolved.
Show resolved Hide resolved

This file was deleted.

35 changes: 35 additions & 0 deletions common-content/en/module/tools/converted-program/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
+++
title = "Putting it all together"
headless = true
time = 30
facilitation = false
emoji= "📖"
hide_from_overview = true
objectives = [
]
+++

Instead of calling `console.log`, in Python we call `print`.

```python
import argparse

parser = argparse.ArgumentParser(
prog="count-containing-words",
description="Counts words in a file that contain a particular character",
)

parser.add_argument("-c", "--char", help="The character to search for", default="-")
parser.add_argument("path", help="The file to search")

args = parser.parse_args()

with open(args.path, "r") as f:
content = f.read()
words_containing_char = len(filter(lambda word: args.char in word, content.split(" ")))
print(words_containing_char)
```

This looks pretty similar to the JavaScript version. The essential shape is the same. But every line is a least a little bit different.
illicitonion marked this conversation as resolved.
Show resolved Hide resolved

Some programming languages are very different, as different as Mandarin and English. But JavaScript and Python are, essentially, quite similar, like Spanish and Portugese.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
+++
title = "Converting JavaScript to Python"
headless = true
time = 40
facilitation = false
emoji= "📖"
objectives = [
"Rewrite JavaScript code as Python"
]
+++

### Parsing command line flags

In JavaScript, we wrote this code (note: there was some other code in between some of these lines):
illicitonion marked this conversation as resolved.
Show resolved Hide resolved

```js
import { program } from "commander";

program
.name("count-containing-words")
.description("Counts words in a file that contain a particular character")
.option("-c, --char <char>", "The character to search for", "-");

program.parse();

const argv = program.args;
const path = argv[0];
const char = program.opts().char;
```

The _essential_ goals here are to:
illicitonion marked this conversation as resolved.
Show resolved Hide resolved
* Allow a user to pass a `-c` argument (defaulting to `-` if they don't).
* Allow a user to pass a path as a positional argument.
* Supply a nice `--help` implementation to help a user if they don't know how to use our tool.

We _accidentally_ did a lot of things to achieve these goals. We used a library called commander. We imported that library. We called some particular functions, and made some particular variables.

If we want to work out how to do this in Python, we should focus on the essential goals. We may want to search for things like "Parse command line flags Python" and "Default argument values Python" because they get to the essential problems we're trying to solve.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then here, once they have identified the essential goals, have them work through it goal by goal.

Show them how to solve 1-2 goals and then have them solve at least one themselves. (One is probably enough)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit torn on this.

Currently what I'm doing is giving them all of the building blocks (introducing with, introducing list comprehensions), and then having them apply them themselves in the backlog item to implement cat, ls, and wc in Python.

I do think it's a little unfriendly to not introduce things like with blocks and list comprehensions - this feels like useful reference material.

Maybe the middle-ground is to ask them to implement the whole thing at the bottom of this block, but then keep the following blocks which show them how to?


Searching Google for "Parse command line flags Python" brought us to [the Python argparse documentation](https://docs.python.org/3/library/argparse.html). The example code looks pretty similar to what we were doing in Python. We can probably write something like:

```python
import argparse

parser = argparse.ArgumentParser(
prog="count-containing-words",
description="Counts words in a file that contain a particular character",
)

parser.add_argument("-c", "--char", help="The character to search for", default="-")
parser.add_argument("path", help="The file to search")

args = parser.parse_args()
```

There are some differences here.
* With commander we were calling functions on a global `program`, whereas with argparse we construct a new `ArgumentParser` which we use.
* `add_argument` takes separate parameters for the short (`-c`) and long (`--char`) forms of the option - `commander` expected them in one string.
* The Python version uses a lot of named arguments (e.g. `add_argument(...)` took `help=`, `default=`), whereas the JavaScript version (`option(...)`) used a lot of positional ones.
* The Python version handles positional arguments itself as arguments with names (`path`), whereas the JavaScript version just gives us an array of positional arguments and leaves us to understand them.

### Validating command line flags

In our JavaScript code, we needed to check that there was exactly one positional argument.

We don't need to do this in our Python code. Because `argparse` treats positional arguments as arguments, it actually already errors if we pass no positional arguments, or more than one.

So we can tick this essential requirement off our list. Different languages or libraries do things differently, and that's ok!

> [!TIP]
> We don't need to convert every line.
>
> We're trying to convert _essential requirements_.
46 changes: 46 additions & 0 deletions common-content/en/module/tools/counting-words/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
+++
title = "Counting words containing a character"
headless = true
time = 15
facilitation = false
emoji= "📖"
hide_from_overview = true
objectives = [
]
+++

In JavaScript we wrote:

```js
content.split(" ").filter((word) => word.indexOf(char) > -1).length
```

What JavaScript calls arrays, Python calls lists. Arrays and lists are basically the same.

Googling for "Python filter list" suggests there are two things we can use - a `filter` function, or something called a "list comprehension". Some people prefer one, other people prefer the other.

Using filter (`lambda` is a keyword for making an anonymous function in Python):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Using filter (`lambda` is a keyword for making an anonymous function in Python):
Using filter (`lambda` is a keyword for making an anonymous function in Python):

This aside is muddying the waters. Suggest here another mini activity.

Guide them to the docs or best place to find this information. Show the code in Javascript again. Then convert it to filter. Then have them convert it list comprehension.


```python
filter(lambda word: args.char in word, content.split(" "))
```

Using a list comprehension:

```python
[word for word in content.split(" ") if args.char in word]
```

Then we need to get the length of the produced list. Googling "python length of list" tells us we wrap our list in a call to `len()`, giving:
illicitonion marked this conversation as resolved.
Show resolved Hide resolved

```python
len([word for word in content.split(" ") if args.char in word])
```

or

```python
len(filter(lambda word: args.char in word, content.split(" ")))
```

The list comprehension version of this works. The `filter` version gives an error. We can try to understand and fix the error, or just use the list comprehension version.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you want them to do here? Be clearer about what you want, don't hint.

114 changes: 114 additions & 0 deletions common-content/en/module/tools/first-nodejs-program/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
+++
title = "Writing a NodeJS program"
headless = true
time = 60
facilitation = false
emoji= "🛠️"
hide_from_overview = true
objectives = [
"Write a zero-dependencies NodeJS program",
]
+++

Below we have a small NodeJS program. It is a bit like `wc`. It counts words in a file. Specifically, it counts words which contain a hyphen (`-`) character.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Below we have a small NodeJS program. It is a bit like `wc`. It counts words in a file. Specifically, it counts words which contain a hyphen (`-`) character.
Below we have a small NodeJS program. It is a bit like `wc`. It counts words which contain a hyphen (`-`) character. Our program is called hyphenWC.


It accepts one command line argument - the path of the file to read and count.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
It accepts one command line argument - the path of the file to read and count.
hyphenWC accepts one command line argument - the path of the file to read and count.


Its output to stdout is just the number of words which contain a hyphen.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Its output to stdout is just the number of words which contain a hyphen.
hyphenWC's output to stdout is the number of words which contain a hyphen.


It uses the same language (JavaScript) as we've written before, but uses some different APIs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
It uses the same language (JavaScript) as we've written before, but uses some different APIs.
hyphenWC uses the same language (JavaScript) we've written before, but uses some different APIs.


```js
import process from "node:process";
import { promises as fs } from "node:fs";

const argv = process.argv.slice(2);
if (argv.length != 1) {
console.error(`Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`);
process.exit(1);
}
const path = argv[0];

const content = await fs.readFile(path, "utf-8");
const wordsContainingHyphens = content.split(" ").filter((word) => word.indexOf("-") > -1).length;
console.log(wordsContainingHyphens);
```

Let's play computer with this program - line by line:

```js
import process from "node:process";
```

This `import` is loading some code from somewhere that isn't this file.

We've seen `import` before. Here, instead of importing from a file we've written, we're importing the `process` module which is built into NodeJS.

This is an example of the same language features (`import`) being used slightly differently (the `"node:"` is a special prefix to say "specially from node").

The `process` module is built into NodeJS for managing our process. We can use it to find out what arguments were passed to the process when it started, find out what user ran the process, exit the process, and more.

```js
import { promises as fs } from "node:fs";
```

We're importing another module.

The `fs` module is built into NodeJS for interacting with the filesystem.

This time, we're not importing the whole module. We are destructuring. The `node:fs` module exposes an object, and we are saying "import the `promises` property from the `fs` module, and bind it to the name `fs`".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This time, we're not importing the whole module. We are destructuring. The `node:fs` module exposes an object, and we are saying "import the `promises` property from the `fs` module, and bind it to the name `fs`".
This time, we're not importing the whole module. We are destructuring. The `node:fs` module exposes an object, and we are saying "import the `promises` property from the `fs` module, and bind it to the name `fs`".

tooltip on destructuring


It's the equivalent to us writing `import { promises } from "node:fs"; const fs = promises;`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
It's the equivalent to us writing `import { promises } from "node:fs"; const fs = promises;`.
It is like writing `import { promises } from "node:fs"; const fs = promises;`.


We are doing this because many of the things in the `fs` module don't support `async`/`await`, but `fs` has a sub-module called `promises` where everything supports `async`/`await`. Because we want to use `async`/`await`, we will use that. But having to write `fs.promises.readFile` is a bit annoying, so instead we import `fs.promises` as if it was just named `fs`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though I know what this means, I don't feel I quite know what this means. Shall we go round on it again?


```js
const argv = process.argv.slice(2);
```

We're getting the `argv` array from the `process` module, and slicing it. We can see in [the `process.argv` documentation](https://nodejs.org/api/process.html#processargv) that `process.argv[0]` will be the path to `node`, and `process.argv[1]` will be the path to this file. We don't care about those, so we'll skip them - as far as we're concerned the arguments start at index 2.

Again, `Array.slice` is exactly the same as we know from JavaScript, but `process.argv` is a new API we can use to get the array we need.

```js
if (argv.length != 1) {
console.error(`Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`);
process.exit(1);
}
```

We always expect our program to be given exactly one argument. Here we check this using an `if` statement, just like we've seen before.

`console.error` writes a message to stderr (which is where error messages should go).

`process.exit` is a function which, when called, will stop our program running. Passing a non-zero number to it indicates that our program did not succeed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit worried we are reproducing the docs here. How can we prompt them to look up what this simple function does first?


```js
const path = argv[0];
```

Giving a useful name to our argument.

```js
const content = await fs.readFile(path, "utf-8");
```

Reading the file at the path passed as an argument. We're using the `fs` module here from `node`, but everything else is just JavaScript - declaring a variable, using `await` because `fs.promises.readFile` is an `async` function, calling a function.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we hide some of these and have them do the play computer and then read yours? Not all, but one or two?


```js
const wordsContainingHyphens = content.split(" ").filter((word) => word.indexOf("-") > -1).length;
```

Just some regular JavaScript. Taking a string, splitting it into an array, filtering the array, searching strings to see if they contain characters, and getting the length of an array.

```js
console.log(wordsContainingHyphens);
```

`console.log` in a NodeJS environment logs to stdout, so this outputs our result to stdout.

{{<note type="Exercise">}}
Save the above program into a file. Run the file with `node`, and count how many words contain hyphens in a few different files.

If you run into problems, ask for help.
{{</note>}}

This file was deleted.

13 changes: 0 additions & 13 deletions common-content/en/module/tools/implement-tools-in-nodejs/index.md

This file was deleted.

13 changes: 0 additions & 13 deletions common-content/en/module/tools/implement-tools-in-python/index.md

This file was deleted.

Loading
Loading