Kartones Blog

Be the change you want to see in this world

Course Review: Learning Kubernetes (LinkedIn Learning)

Review

Continuing with my broadening of topics, now it's time to learn some Kubernetes basics, to at least know what it is capable of, so I picked up Learning Kubernetes, a 2.5 hours Linkedin Learning course that looked nice. Touching topics from building blocks to deployments, jobs, services, labels, data management and logging, while not in depth, it is a good overview, with many hands-on examples.

But the course also has two negative points I wanted to point out. The smaller one is the fact that there are no Linux setup instructions! Mac and Windows yes, and for sure if you're like me will probably use a cloud provider rather than setup my own k8s cluster, but still 😒. The second one is also not critical, but more annoying, because relates with the pacing of the course. Sometimes, examples are really long, slowly going step by step, and repeating the previous command just adding a --show-labels flag, or reading trivial details; Other times, and despite watching it at 1x speed, the author rushes over non-trivial yaml files, executes a command and "voila, that's done!", or does a kubectl describe deployment and mentions so fast the relevant output sections (not even the content) that I had to pause the video, go back and check it. It also happens with the slides presented, he advances slides at times so quickly that you can't even read the latest bullet point or sentence when appears.

Overall, I felt satisfied with the contents. Just be prepared to pause from time to time.

Notes

The author recommends reading Kelsey Hightower's Kubernetes The Hard Way so probably that will be my next step.


Course Review: Learning Docker (LinkedIn Learning)

Review

I wanted to refresh my Docker knowledge, as it is been a while since I've done anything non-trivial. Learning Docker is an almost 3 hours course that teaches the basics, but covers quite a lot of topics; I'd say that almost if not all of the relevant ones. It is really well done, and goes through a lot of content, but well explained, and with very good examples. I was surprised with the samples because, with a topic like containers, you can easily get into boring things, but using netcat to show how networks in Docker are created and function, showing the relation with iptables, or a very clear example of the Dockerfile alters the image layers, are only a brief list of how you'll learn.

Recommended not only because of the content, but also because of the form.


Javascript Named Exports for testing

I'm now using Jest and its useful mocking extensions quite often at work, and while the combination of modern Javascript + NodeJS+ Typescript + third-party packages + your code sometimes makes things quite a challenge to test, with effort (and a few refactors to ease testability) most times I feel satisfied with the ending results.

Javascript imports via import (MDN reference) are quite flexible, but on the other hand being modules means that you either export something, or you don't and then is not available from the outside, so it's not as easy to test the unexposed code fragments. A colleague at work explained to me a testing pattern for that scenario, but I haven't found it online, so this small post is both a personal note and a documentation attempt.

The Testables Named Export Pattern

The main idea is using javascript named exports to export a specific group for testing with a meaningful name (e.g. testables). This way, you do add one additional export, but only one, also clearly signalling the intention; and you don't alter logic elsewhere in the code.

Let's use a tiny example:

a-file.ts

export function aMethod() { 
  ... 
  const result = aNonExportedMethod('something');
  anotherNonExportedMethod();
  ...
}

export function anotherMethod() { ... }

aNonExportedMethod(param : string) { ... }

anotherNonExportedMethod(); { ... }

export const testables = {
  aNonExportedMethod,
  anotherNonExportedMethod,
};

Your normal code will do normal imports, and simply won't care about testables nor be affected by it:

another-file.ts

import { aMethod, anotherMethod } from './a-file';

...

But when writing your tests, you will now be able to also import the testables group, so that you can write tests focused on that logic:

a-file.test.ts

import { aMethod, anotherMethod, testables } from './a-file';

...

const result = testables.aNonExportedMethod('a-string');
expect(result).toEqual(...);

Closing Notes

I am assuming no default export, but the behaviour is pretty much the same otherwise. Just don't return the testables as the default export if you use it (I don't fancy much default exports, and seems I'm not the only one).

Be careful as module logic will still execute upon import (any Javascript/Typescript outside functions will run) no matter if you only import { testables }. This can bite you but in general it is a good practice to avoid "import work" so keep it in mind.

I know not everyone will like this approach and could argue that you should only test the visible surface (even more so if the non-exported methods are called inside the exported ones). But I at least prefer smaller, more focused tests so, in my example, after testing testables.aNonExportedMethod and testables.anotherNonExportedMethod the test(s) about aMethod would be much smaller and focused on the logic not already covered by others.


Object Oriented Python - A Summary

I love Python, but you can notice how object orientation was an afterthought (with some wanky compiler tricks) when you learn how its object-oriented system works. It is powerful, but some concepts hard to grasp, and not always properly explained out there. Hence, the main reasoning behind writing this post.

It is not an exhaustive guide, but I think I've touched the main points and should be a decent enough quick reference guide.

Basic definitions

  • Class: What we define. Kind of the structure of which we'll create instances of. Contains methods, attributes and properties.
  • Instance: Also commonly to what we refer when we say Object. A specific entity that has everything that a certain Class defines, with it's own values/state. One class can have many instances.
  • Simple Inheritance: When a class has only a single parent class. A class inherits methods, attributes, properties, etcetera from the parent class.
  • Multiple Inheritance: When a class has more than one parent class. Python supports multiple inheritance.
  • Attribute: A variable a class defines, and thus, that every instance will contain, each with a different value. An attribute might be public, protected or private.
  • Class Attribute: A variable that exists on the class itself, and thus is shared (and accessible) by all instances (if any).
  • Property: A function or functions (getter and/or setter), that wrap an attribute and allow to also add logic around operating with it, while keeping the usage syntax same as if using an attribute (e.g. my instance.an_attribute -> my_instance.a_property )
  • Method: A function defined inside a class. Same as attributes, can be marked as protected or private, but really all are public. More on methods later.
  • Constant: Python has no constants, but by convention a variable or class attribute that is all uppercased is considered a constant.
  • Abstract classes and abstract methods: Python got support for abstract classes and methods late in its life, so you need to use the abc base package. This tutorial won't cover them as they are not very widely used, but if you want info, check the official documentation for ABCs.

Class creation basics

class Person:
  A_CONSTANT = 33

  a_class_attribute = 'Something'

  # Class constructor with two parameters (`self` is always required)
  def __init__(self, name, address):
    # private instance attribute
    self.__name = name
    # public instance attribute
    self.address = address

  # getter, to do for example: print(myInstance.name)
  @property
  def name(self):
    return self.__name

  # setter, to do for example: myInstance.name = 'New name'
  @name.setter
  def name(self, value):
    self.__name = value

  # `self` is always required for normal class methods
  def a_method(self, a_parameter):
    # note that despite being a class attribute, we can also access `a_class_attribute` through `self`
    print(`{name} {attr} {param}`.format(name=self.name, attr=self.a_class_attribute, param=a_parameter))

Creating new instances of a class is also pretty simple:

a_person = Person('John Doe', 'somewhere')
another_person = Person('Jane Doe', 'somewhere else')

In python we signal with _ protected methods and attributes, and if we "really" want to make them private, we can use __ (two underscores) and, when compiling the class, the Python interpreter will change the attribute name so outside callers can't (so easily) access them.

Remarks

There is also a property deleter, but it is not commonly used.

Do not rely on scope modifiers for security, just for explaining to users of your class the intended interface (what you expose for external usage). You can always access protected attributes and methods, and with a little extra work also the private ones.

You will often find classes simply exposing attributes instead of using properties. Properties can also be added using special functions, but the decorators are cleaner, easier and preferred.

You can dynamically add new attributes anywhere from a class, and in fact you will see this behaviour at many professional and open-source projects. However, a good convention for future code readers is defining all of them inside the constructor.

Class vs Static vs Instance variables and methods

In Python, there are really two state-related scopes for variables and methods: class/static and instance. A variable either lives at each and every instance of a class, or then it lives as a class variable and is shared among all instances; and same goes with methods.

class Person:
  a_class_attribute = 'something'

  def __init__(self):
    self.an_instance_attribute = 'something else'

  @staticmethod
  def a_static_method(a_param):
    print(Person.a_class_attribute, a_param)

  def an_instance_method(self, a_param):
    print(self.an_instance_attribute, a_param)

But when writing classes, we can actually define methods in a third way, as a "class method". This is mostly syntactic sugar, created in my opinion to help you write clean code, and I'll explain why after explaining what it is.

A class method is defined with the @classmethod decorator, and does have an additional first "magic" parameter like self, in this case cls. That parameter allows us to call to the class itself, without repeating its name; other than that, works exactly the same as a @staticmethod:

class Person:
  a_class_attribute = 'something'

  @classmethod
  def a_static_method(cls, a_param):
    print(cls.a_class_attribute, a_param)

Now, what's the point of having @classmethod if does the same as @staticmethod but with more magic? Well, the intention is to write code Python style, by adhering to the following rules when writing methods for your classes:

  • If you need to use or alter instance variables, you need self so it is an instance method
  • If you need to use or alter class variables, you should use cls and @classmethod
  • If neither of the above, then you should use @staticmethod

Let's rewrite and expand Person applying the rules:

class Person:
  a_class_attribute = 'something'

  def __init__(self):
    self.an_instance_attribute = 'something else'

  @staticmethod
  def a_static_method(a_param):
    print(a_param)

  @classmethod
  def another_static_method(cls, a_param):
    print(cls.a_class_attribute, a_param)

  def an_instance_method(self, a_param):
    print(self.an_instance_attribute, a_param)

Using cls when calling static stuff of the own class means you can easily refactor its name without needing to grep the code. There should be a single occurrence in the class definition file, and most IDEs will color and hint and the like the same as using Person.xxxxx.

As I like to sometimes go into the internals of things, to help hold better the concepts, the following snippet shows how you would call each method, and how Python really calls them, which I think it's interesting and revealing:

a_person = Person()

# how you'll call it:
a_person.an_instance_method('A')
# how python calls it:
Person.an_instance_method(a_person, 'A')

# you:
a_person.a_static_method('B')
Person.a_static_method('B')
# python:
Person.a_static_method('B')

# you:
a_person.another_static_method('C')
Person.another_static_method('C')
# python:
Person.another_static_method(Person, 'C')

As we just saw, objects really behave like static classes to which Python passes the instance to operate with. A clever trick but confusing at first nonetheless.

Remarks

You can always use self. from instance methods to refer to class attributes, just take into account that it will first check instance attributes, then class attributes.

You can also overload operators and certain special methods (comparators, string conversion, etcetera).

Inheritance

class A:
  def __init__(self, something):
    pass

  def a_method(self):
    pass

class B(A):
  def __init__(self, something):
    super().__init__(something)

  def a_method(self):
    super().a_method()

class C:
  def __init__(self, something):
    pass

  def a_method(self):
    pass

  def another_method(self):
    pass


class D(B, C):
  def __init__(self, something):
    # calls B's constructor
    super().__init__(something)

  def a_method(self):
    # calls B's a_method
    super().a_method()

To call a parent class method, you use super(). In the example above, we use it to call A's constructor from B's, passing the something parameter (pretty useless example, I know, there are way better ones out there).

What happens when you have multiple-inheritance and two or more parent classes have the same method overloaded? In short, python's method resolution order works from left to right regarding how you defined the parent classes: in the previous example B's a_method takes precedence over C's a_method if D did something like super().a_method() somewhere.

Method Overloading

Python does not support method overload, meaning that you can only have one a_method() inside a given class. In other languages, like for example C#, you could have one overload with 2 parameters, another with 3, etcetera. Not in Python.

What you can do is override a parent class method inside a child class. We already saw this in the previous section.

Closing thoughts

This was a brief introduction to Python's OOP. If you wish to really get into the details, I strongly recommend visiting the website realpython.com, which contains many very detailed and nicely explained articles about most of the language features.


Course Review: Monorepos - A Beginner's Guide (Udemy)

Review

Monorepos: A Beginner's Guide is a ~40 minutes 2020 small course that, despite it's catchy but generic name, really should be titled something like "Multi-package and Monorepo NodeJS projects using Yarn".

The topics covered are:

  • Now NodeJS searches for and loads modules/packages
  • Using yarn link
  • Using yarn workspaces
  • package hoisting when using workspaces
  • cross-package bin scripts
  • Using yarn workspace
  • A bonus 10 minutes lesson about lerna, covering the basic commands, scoped packages and npm publishing of monorepo packages using the tool

Although you can apply the same principles and actions with npm 7+, all examples are explained and performed via yarn.

It is a very compact course, very very focused on node + yarn, but if that's what you search for, it is concise, direct to the point and for once, quicker than reading through official documentation.

My only concern is that the initial approach of manually placing modules inside node_modules folder, although if mentioned as a sub-optimal solution, it is a really ugly workaround 😵‍💫 (the author even provides instructions to properly un-gitignore and commit the hack!)


Previous entries