mirror of
https://github.com/ItsDrike/itsdrike.com.git
synced 2025-02-24 00:29:03 +00:00
Fix formatting in various posts
This commit is contained in:
parent
523fefed1b
commit
c5b1c9da0a
7 changed files with 64 additions and 50 deletions
|
@ -89,7 +89,7 @@ Consider this code:
|
||||||
```
|
```
|
||||||
|
|
||||||
In the example here, we can see that python keeps a reference count for the empty list object, and in this case, it was
|
In the example here, we can see that python keeps a reference count for the empty list object, and in this case, it was
|
||||||
3. The list object was referenced by a, b and the argument passed to `sys.getrefcount`. If we didn't have locks,
|
\3. The list object was referenced by a, b and the argument passed to `sys.getrefcount`. If we didn't have locks,
|
||||||
threads could attempt to increase the reference count at once, this is a problem because what would actually happen
|
threads could attempt to increase the reference count at once, this is a problem because what would actually happen
|
||||||
would go something like this:
|
would go something like this:
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ would go something like this:
|
||||||
|
|
||||||
[Treat sections of 2 lines as things happening concurrently]
|
[Treat sections of 2 lines as things happening concurrently]
|
||||||
|
|
||||||
You can see that because threads 1 and 2 both read the reference amount from memory at the same time, they read the
|
You can see that because threads 1 and 2 both read the reference amount from memory at the same time, they read the
|
||||||
same number, then they've increased it and stored it back without ever knowing that some other thread is also in the
|
same number, then they've increased it and stored it back without ever knowing that some other thread is also in the
|
||||||
process of increasing the reference count but it read the same amount from memory as this process, so even though the
|
process of increasing the reference count but it read the same amount from memory as this process, so even though the
|
||||||
first thread stored the updated amount, the 2nd thread also stored the updated amount, except they were the same
|
first thread stored the updated amount, the 2nd thread also stored the updated amount, except they were the same
|
||||||
|
@ -167,6 +167,7 @@ become incorrect in a way that's hard to see during code reviews.
|
||||||
## Debugging multi-threaded code
|
## Debugging multi-threaded code
|
||||||
|
|
||||||
As an example, this is a multi-threaded code that will pass all tests and yet it is full of bugs:
|
As an example, this is a multi-threaded code that will pass all tests and yet it is full of bugs:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
@ -183,6 +184,7 @@ for _ in range(5):
|
||||||
threading.Thread(target=foo).start()
|
threading.Thread(target=foo).start()
|
||||||
print("Finished")
|
print("Finished")
|
||||||
```
|
```
|
||||||
|
|
||||||
When you run this code, you will most likely get a result that you would expect, but it is possible that you could also
|
When you run this code, you will most likely get a result that you would expect, but it is possible that you could also
|
||||||
get a complete mess, it's just not very likely because the code runs very quickly. This means you can write code
|
get a complete mess, it's just not very likely because the code runs very quickly. This means you can write code
|
||||||
multi-threaded code that will pass all tests and still fail in production, which is very dangerous.
|
multi-threaded code that will pass all tests and still fail in production, which is very dangerous.
|
||||||
|
@ -192,6 +194,7 @@ behind every instruction to ensure that it is safe if a switch happens during th
|
||||||
it is advised to run the code multiple times because there is a chance of getting the correct result even with this
|
it is advised to run the code multiple times because there is a chance of getting the correct result even with this
|
||||||
method since it always is one of the possibilities, this is why multi-threaded code can introduce a lot of problems.
|
method since it always is one of the possibilities, this is why multi-threaded code can introduce a lot of problems.
|
||||||
This would be the code with this "fuzzing" method applied:
|
This would be the code with this "fuzzing" method applied:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
@ -219,6 +222,7 @@ for _ in range(5):
|
||||||
threading.Thread(target=foo).start()
|
threading.Thread(target=foo).start()
|
||||||
print("Finished")
|
print("Finished")
|
||||||
```
|
```
|
||||||
|
|
||||||
You may also notice that I didn't just add `fuzz()` call to every line, I've also split the line that incremented
|
You may also notice that I didn't just add `fuzz()` call to every line, I've also split the line that incremented
|
||||||
counter into 2 lines, one that reads the counter and another one that actually increments it, this is because
|
counter into 2 lines, one that reads the counter and another one that actually increments it, this is because
|
||||||
internally, that's what would be happening it would just be hidden away, so to add a delay between these instructions
|
internally, that's what would be happening it would just be hidden away, so to add a delay between these instructions
|
||||||
|
@ -226,6 +230,7 @@ I had to actually split the code like this. This makes it almost impossible to t
|
||||||
problem.
|
problem.
|
||||||
|
|
||||||
It is possible to fix this code with the use of locks, which would look like this:
|
It is possible to fix this code with the use of locks, which would look like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
@ -257,13 +262,15 @@ for t in worker_threads:
|
||||||
with printer_lock:
|
with printer_lock:
|
||||||
print("Finished")
|
print("Finished")
|
||||||
```
|
```
|
||||||
|
|
||||||
As we can see, this code is a lot more complex than the previous one, it's not terrible, but you can probably imagine
|
As we can see, this code is a lot more complex than the previous one, it's not terrible, but you can probably imagine
|
||||||
that with a bigger codebase, this wouldn't be fun to manage.
|
that with a bigger codebase, this wouldn't be fun to manage.
|
||||||
|
|
||||||
Not to mention that there is a core issue with this code. Even though the code will work and doesn't actually have any
|
Not to mention that there is a core issue with this code. Even though the code will work and doesn't actually have any
|
||||||
bugs, it is still wrong. Why? When we use enough locks in our multi-threaded code, we may end up making it full
|
bugs, it is still wrong. Why? When we use enough locks in our multi-threaded code, we may end up making it full
|
||||||
sequential, which is what happened here. Our code is running synchronously, with huge amount of overhead from the locks
|
sequential, which is what happened here. Our code is running synchronously, with huge amount of overhead from the locks
|
||||||
that didn't need to be there and the actual code that would've been sufficient looks like this:
|
that didn't need to be there and the actual code that would've been sufficient looks like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
counter = 0
|
counter = 0
|
||||||
print("Starting")
|
print("Starting")
|
||||||
|
@ -273,6 +280,7 @@ for _ in range(5)
|
||||||
print("----------------------")
|
print("----------------------")
|
||||||
print("Finished")
|
print("Finished")
|
||||||
```
|
```
|
||||||
|
|
||||||
While in this particular case, it may be pretty obvious that there was no need to use threading at all, there are a lot
|
While in this particular case, it may be pretty obvious that there was no need to use threading at all, there are a lot
|
||||||
of cases in which it isn't as clear and I have seen some projects with code that could've been sequential but they were
|
of cases in which it isn't as clear and I have seen some projects with code that could've been sequential but they were
|
||||||
already using threading for something else and so they made use of locks and added some other functionality, which made
|
already using threading for something else and so they made use of locks and added some other functionality, which made
|
||||||
|
|
|
@ -58,18 +58,22 @@ youtube-dl, download the video and then stream it from our machine instead of fr
|
||||||
download the file from our server that has now downloaded this video, however that's way too crude.
|
download the file from our server that has now downloaded this video, however that's way too crude.
|
||||||
|
|
||||||
There is a much nicer method that we can use, and it is still utilizing pure SSH:
|
There is a much nicer method that we can use, and it is still utilizing pure SSH:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ssh -f -N -D 1080 user@server
|
ssh -f -N -D 1080 user@server
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will start SSH in background (`-f`), it won't run any actual commands (`-N`) and it will be bound to the
|
This command will start SSH in background (`-f`), it won't run any actual commands (`-N`) and it will be bound to the
|
||||||
port 1080 on our machine (`-D`). This means that we can utilize this port as a SOCK and make our server act as SOCKS5
|
port 1080 on our machine (`-D`). This means that we can utilize this port as a SOCK and make our server act as SOCKS5
|
||||||
proxy. This kind of proxy will even be supported by most web browsers, allowing you to simply specify the address
|
proxy. This kind of proxy will even be supported by most web browsers, allowing you to simply specify the address
|
||||||
(in our case `127.0.0.1:1080`) and have all traffic go through this external server.
|
(in our case `127.0.0.1:1080`) and have all traffic go through this external server.
|
||||||
|
|
||||||
To test that this connection really does work, we could use the `curl` command like this:
|
To test that this connection really does work, we could use the `curl` command like this:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --max-time 3 -x socks5h://127.0.0.1:1080 https://itsdrike.com
|
curl --max-time 3 -x socks5h://127.0.0.1:1080 https://itsdrike.com
|
||||||
```
|
```
|
||||||
|
|
||||||
If we see the HTML code as the output, it means that we've obtained the content of the specified website through our
|
If we see the HTML code as the output, it means that we've obtained the content of the specified website through our
|
||||||
socks5 proxy, that we've established through simple SSH.
|
socks5 proxy, that we've established through simple SSH.
|
||||||
|
|
||||||
|
@ -93,9 +97,11 @@ around SSH and it will simply utilize SSH in the background, which is also why w
|
||||||
server side for this to work properly, as long as we simply have the SSH server running, `sshuttle` will work fine.
|
server side for this to work properly, as long as we simply have the SSH server running, `sshuttle` will work fine.
|
||||||
|
|
||||||
We can use sshuttle with a command like this:
|
We can use sshuttle with a command like this:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo sshuttle -r user@machine 172.67.161.205/24 -vv
|
sudo sshuttle -r user@machine 172.67.161.205/24 -vv
|
||||||
```
|
```
|
||||||
|
|
||||||
Which will forward all traffic destined for the particular address block (the IP/number is called the CIDR notation, it
|
Which will forward all traffic destined for the particular address block (the IP/number is called the CIDR notation, it
|
||||||
essentially specifies which IPs should be affected depending on the number after /, you can read more about it on
|
essentially specifies which IPs should be affected depending on the number after /, you can read more about it on
|
||||||
[wikipedia](https://wikiless.org/wiki/Classless_Inter-Domain_Routing?lang=en)). In this case, I've specified the IP of
|
[wikipedia](https://wikiless.org/wiki/Classless_Inter-Domain_Routing?lang=en)). In this case, I've specified the IP of
|
||||||
|
@ -112,12 +118,14 @@ you need to think about this ahead of time.
|
||||||
|
|
||||||
You could also simply redirect the port 22 to something else using iptables instead of having to mess with the SSH
|
You could also simply redirect the port 22 to something else using iptables instead of having to mess with the SSH
|
||||||
config. You would do that with this command:
|
config. You would do that with this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo iptables -t nat -I PREROUTING -p tcp --dport 1234 -j REDIRECT --to-ports 22
|
sudo iptables -t nat -I PREROUTING -p tcp --dport 1234 -j REDIRECT --to-ports 22
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will make port `1234` act as the SSH port, and you could then access the server by specifying this port
|
This command will make port `1234` act as the SSH port, and you could then access the server by specifying this port
|
||||||
instead of the default port in the ssh command:
|
instead of the default port in the ssh command:
|
||||||
|
|
||||||
```
|
```
|
||||||
ssh -f -N -D 1080 user@server -p 1234
|
ssh -f -N -D 1080 user@server -p 1234
|
||||||
```
|
```
|
||||||
|
@ -213,9 +221,11 @@ Turns out that even with a security measure as strict as only allowing access to
|
||||||
somewhat make our way to our server, by essentially telling it to map all exiting traffic from port 443 to port 22.
|
somewhat make our way to our server, by essentially telling it to map all exiting traffic from port 443 to port 22.
|
||||||
|
|
||||||
To do this, we would use a command like this:
|
To do this, we would use a command like this:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ssh -o "ProxyCommand nc -X connect -x proxy_server:3128 our_server_IP 443" user@our_server_IP
|
ssh -o "ProxyCommand nc -X connect -x proxy_server:3128 our_server_IP 443" user@our_server_IP
|
||||||
```
|
```
|
||||||
|
|
||||||
Here we're essentially sending a proxy command to the web proxy server (listening on port 3128) to through the port 443
|
Here we're essentially sending a proxy command to the web proxy server (listening on port 3128) to through the port 443
|
||||||
to our_server_IP and make requests to the SSH's default port (22) on our_server_IP. Making the actual proxy server
|
to our_server_IP and make requests to the SSH's default port (22) on our_server_IP. Making the actual proxy server
|
||||||
access our server on port 22.
|
access our server on port 22.
|
||||||
|
@ -235,8 +245,10 @@ really be possible.
|
||||||
|
|
||||||
To explain how easy it is to discover something like this, basically all that's needed is to run a single command on
|
To explain how easy it is to discover something like this, basically all that's needed is to run a single command on
|
||||||
that web proxy:
|
that web proxy:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
iptables -t nat -L
|
iptables -t nat -L
|
||||||
```
|
```
|
||||||
|
|
||||||
And look for the output policy destinations. Even though many network admins won't do this, you shouldn't ever risk
|
And look for the output policy destinations. Even though many network admins won't do this, you shouldn't ever risk
|
||||||
doing something silly like this, because if you will get discovered, you could get into some serious trouble
|
doing something silly like this, because if you will get discovered, you could get into some serious trouble
|
||||||
|
|
|
@ -3,24 +3,24 @@ title: Managing (multiple) git credentials
|
||||||
date: 2022-07-27
|
date: 2022-07-27
|
||||||
tags: [programming, git]
|
tags: [programming, git]
|
||||||
sources:
|
sources:
|
||||||
- <https://docs.github.com/en/get-started/getting-started-with-git/caching-your-github-credentials-in-git>
|
- <https://docs.github.com/en/get-started/getting-started-with-git/caching-your-github-credentials-in-git>
|
||||||
- <https://docs.github.com/en/authentication/connecting-to-github-with-ssh>
|
- <https://docs.github.com/en/authentication/connecting-to-github-with-ssh>
|
||||||
- <https://www.onwebsecurity.com/configuration/git-on-windows-location-of-global-configuration-file.html>
|
- <https://www.onwebsecurity.com/configuration/git-on-windows-location-of-global-configuration-file.html>
|
||||||
- <https://security.stackexchange.com/questions/90077/ssh-key-ed25519-vs-rsa>
|
- <https://security.stackexchange.com/questions/90077/ssh-key-ed25519-vs-rsa>
|
||||||
- <https://www.shellhacks.com/git-config-username-password-store-credentials/>
|
- <https://www.shellhacks.com/git-config-username-password-store-credentials/>
|
||||||
- <https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage>
|
- <https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage>
|
||||||
- <https://engineeringfordatascience.com/posts/how_to_manage_multiple_git_accounts_on_the_same_machine/>
|
- <https://engineeringfordatascience.com/posts/how_to_manage_multiple_git_accounts_on_the_same_machine/>
|
||||||
- <https://git-scm.com/docs/gitcredentials>
|
- <https://git-scm.com/docs/gitcredentials>
|
||||||
- <https://www.baeldung.com/ops/git-configure-credentials>
|
- <https://www.baeldung.com/ops/git-configure-credentials>
|
||||||
- <https://www.freecodecamp.org/news/manage-multiple-github-accounts-the-ssh-way-2dadc30ccaca/>
|
- <https://www.freecodecamp.org/news/manage-multiple-github-accounts-the-ssh-way-2dadc30ccaca/>
|
||||||
- <https://blog.bitsrc.io/how-to-use-multiple-git-accounts-378ead121235>
|
- <https://blog.bitsrc.io/how-to-use-multiple-git-accounts-378ead121235>
|
||||||
- <https://www.freecodecamp.org/news/the-ultimate-guide-to-ssh-setting-up-ssh-keys/>
|
- <https://www.freecodecamp.org/news/the-ultimate-guide-to-ssh-setting-up-ssh-keys/>
|
||||||
- <https://www.atlassian.com/git/tutorials/setting-up-a-repository/git-config>
|
- <https://www.atlassian.com/git/tutorials/setting-up-a-repository/git-config>
|
||||||
changelog:
|
changelog:
|
||||||
2023-01-30:
|
2023-01-30:
|
||||||
- Add note about disabling commit signing
|
- Add note about disabling commit signing
|
||||||
- Add alternative command for copying on wayland
|
- Add alternative command for copying on wayland
|
||||||
- Fix typos and text wrapping
|
- Fix typos and text wrapping
|
||||||
---
|
---
|
||||||
|
|
||||||
Many people often find initially setting up their git user a bit unclear, especially when it comes to managing multiple
|
Many people often find initially setting up their git user a bit unclear, especially when it comes to managing multiple
|
||||||
|
@ -78,10 +78,9 @@ configured account, you can disable it with:
|
||||||
```bash
|
```bash
|
||||||
git config --local commit.gpgsign false
|
git config --local commit.gpgsign false
|
||||||
```
|
```
|
||||||
|
|
||||||
{{< /notice >}}
|
{{< /notice >}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Git credentials
|
## Git credentials
|
||||||
|
|
||||||
User configuration is one thing, but there's another important part of account configuration to consider, that is
|
User configuration is one thing, but there's another important part of account configuration to consider, that is
|
||||||
|
@ -239,7 +238,6 @@ The cache credential helper will never write your credential data to disk, altho
|
||||||
Unix sockets. These sockets are protected using file permissions that are limited to the user who stored them though,
|
Unix sockets. These sockets are protected using file permissions that are limited to the user who stored them though,
|
||||||
so even in multi-user machine, generally speaking, they are secure.
|
so even in multi-user machine, generally speaking, they are secure.
|
||||||
|
|
||||||
|
|
||||||
#### Custom credential helpers
|
#### Custom credential helpers
|
||||||
|
|
||||||
Apart from these default options, you can also use [custom
|
Apart from these default options, you can also use [custom
|
||||||
|
|
|
@ -144,4 +144,3 @@ this byte-code with an interpreter. This is also known as Just In Time (JIT) com
|
||||||
|
|
||||||
Most languages tend to only be one or the other, but there are a fair few that follow this hybrid implementation, most
|
Most languages tend to only be one or the other, but there are a fair few that follow this hybrid implementation, most
|
||||||
notably, these are: Java, C#, Python, VB.NET
|
notably, these are: Java, C#, Python, VB.NET
|
||||||
|
|
||||||
|
|
|
@ -36,10 +36,10 @@ systems is, as the name would imply, manage the database. It controls how the da
|
||||||
if there should be some internal compression of this database or things like that.
|
if there should be some internal compression of this database or things like that.
|
||||||
|
|
||||||
Even though there are a lot of choices for DBMS, no matter which one we end up using, on the surface, they will be
|
Even though there are a lot of choices for DBMS, no matter which one we end up using, on the surface, they will be
|
||||||
doing exactly the same thing. Storing tables of data. Each item in the database usually has some *primary key*, which
|
doing exactly the same thing. Storing tables of data. Each item in the database usually has some _primary key_, which
|
||||||
is a unique identifier for given column of data. We can also have composite primary keys, where there would be multiple
|
is a unique identifier for given column of data. We can also have composite primary keys, where there would be multiple
|
||||||
slots which are unique when combined, but don't necessarily need to be unique on their own. We can also use what's
|
slots which are unique when combined, but don't necessarily need to be unique on their own. We can also use what's
|
||||||
called a *foreign key*, which is basically the primary key of something in another database table, separate from the
|
called a _foreign key_, which is basically the primary key of something in another database table, separate from the
|
||||||
current one, to avoid data repetition. This would be an example of the data tables that a database could hold:
|
current one, to avoid data repetition. This would be an example of the data tables that a database could hold:
|
||||||
|
|
||||||
Table of students:
|
Table of students:
|
||||||
|
@ -68,8 +68,8 @@ Student Grades:
|
||||||
| ... | ... | ... | ... |
|
| ... | ... | ... | ... |
|
||||||
{{< /table >}}
|
{{< /table >}}
|
||||||
|
|
||||||
Here we can see that the *Student Grades* table doesn't have a standalone unique primary key, like the Students table
|
Here we can see that the _Student Grades_ table doesn't have a standalone unique primary key, like the Students table
|
||||||
has, but rather it has a composite primary key, in this case, it's made of 3 columns: *Student*, *Subject* and *Year*.
|
has, but rather it has a composite primary key, in this case, it's made of 3 columns: _Student_, _Subject_ and _Year_.
|
||||||
We can also see that rather than defining the whole student all over again, since we already have the students table,
|
We can also see that rather than defining the whole student all over again, since we already have the students table,
|
||||||
we can instead simply use the Student ID from which we can look up the given student from this table
|
we can instead simply use the Student ID from which we can look up the given student from this table
|
||||||
|
|
||||||
|
@ -148,4 +148,3 @@ Another use-case for databases is when you need to host the data of the database
|
||||||
database, we can simply expose some port and let the DBMS handle interactions with it when our program can simply be
|
database, we can simply expose some port and let the DBMS handle interactions with it when our program can simply be
|
||||||
making requests to this remote server. This is usually how we handle using databases with servers, but many client-side
|
making requests to this remote server. This is usually how we handle using databases with servers, but many client-side
|
||||||
programs are creating their own local databases and using those, simply because using files is ineffective.
|
programs are creating their own local databases and using those, simply because using files is ineffective.
|
||||||
|
|
||||||
|
|
|
@ -134,8 +134,8 @@ return list(result.values())
|
||||||
To preserve the original elements, we used a dict that held the unique hashable memory ids of our objects as keys and
|
To preserve the original elements, we used a dict that held the unique hashable memory ids of our objects as keys and
|
||||||
the actual unhashable objects as values. Once we were done, we just returned all of the values in it as a list.
|
the actual unhashable objects as values. Once we were done, we just returned all of the values in it as a list.
|
||||||
|
|
||||||
The result of this would be: `[x, y, 1, 2, "hi", Foo(x=5)]`. *(Note that `x`, `y` and `Foo(x=5)` would actually be
|
The result of this would be: `[x, y, 1, 2, "hi", Foo(x=5)]`. _(Note that `x`, `y` and `Foo(x=5)` would actually be
|
||||||
printed in the same way, since they're the same class, sharing the same `__repr__`)*. From this output we can clearly
|
printed in the same way, since they're the same class, sharing the same `__repr__`)_. From this output we can clearly
|
||||||
see that even though `x`, `y`, and `Foo(x=5)` are exactly the same thing, sharing the same attributes, they're
|
see that even though `x`, `y`, and `Foo(x=5)` are exactly the same thing, sharing the same attributes, they're
|
||||||
different objects and therefore they have different memory ids, which means our algorithm didn't remove them, however
|
different objects and therefore they have different memory ids, which means our algorithm didn't remove them, however
|
||||||
there is now only one `x`, because the second one was indeed exactly the same object, so that did get removed.
|
there is now only one `x`, because the second one was indeed exactly the same object, so that did get removed.
|
||||||
|
@ -217,4 +217,3 @@ one if we know which classes will be used there.
|
||||||
Even though we do have ways to deal with unhashables, if you're in control of the classes, and they aren't supposed to
|
Even though we do have ways to deal with unhashables, if you're in control of the classes, and they aren't supposed to
|
||||||
be mutable, always make sure to add a `__hash__` method to them, so that duplicates can be easily removed in `O(n)`
|
be mutable, always make sure to add a `__hash__` method to them, so that duplicates can be easily removed in `O(n)`
|
||||||
without any complicated inconveniences.
|
without any complicated inconveniences.
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,6 @@ Here's a list of some definable generic types that are currently present in pyth
|
||||||
| Mapping[str, int] | Mapping from `str` keys to `int` values (immutable) |
|
| Mapping[str, int] | Mapping from `str` keys to `int` values (immutable) |
|
||||||
{{< /table >}}
|
{{< /table >}}
|
||||||
|
|
||||||
|
|
||||||
In python, we can even make up our own generics with the help of `typing.Generic`:
|
In python, we can even make up our own generics with the help of `typing.Generic`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
Loading…
Add table
Reference in a new issue