GithubHelp home page GithubHelp logo

portfolio-site's Introduction

a-luna's github stats

๐Ÿ‘จ๐Ÿฝโ€๐Ÿ’ป What Type of Code Did I Write This Week?

From: 22 May 2024 - To: 29 May 2024

Svelte       6 hrs 26 mins   โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘   48.00 %
TypeScript   3 hrs 15 mins   โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘   24.25 %
Python       2 hrs 44 mins   โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘   20.42 %
CSS          24 mins         โ–“โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘   03.04 %
Text         15 mins         โ–’โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘   01.89 %

Coding metrics are powered by Wakatime

  • Add Search to Your Static Site with Lunr.js (Hugo, Vanilla JS) Jun 30 2020 โ€” I decided to document how I implemented a search feature with Hugo and Lunr.js on my personal blog site. Since this is a static site the search functionality is performed entirely within the client's browser. My solution uses vanilla JS DOM manipulation to render the search results. I believe that my approach includes features that are markedly different from the implementations I encountered while researching this task, features which enhance the overall search UX.

  • An Introduction to Decorators in Python Feb 27 2020 โ€” Decorators can be a daunting topic when first encountered. While the Zen of Python states "There should be one-- and preferably only one --obvious way to do it", there are many, equally valid ways to implement the same decorator. These different methods can be categorized as either function-based, class-based, or a hybrid of both. In this post I will explain the design and behavior of Python decorators and provide examples of decorators that I frequently use in my own code.

  • Hugo: Add Copy-to-Clipboard Button to Code Blocks with Vanilla JS Nov 13 2019 โ€” Hugo includes a built-in syntax-highlighter called Chroma. Chroma is extremely fast since it is written in pure Go (like Hugo) and supports every language I can think of. Chroma's speed is especially important since syntax highlighters are notorious for causing slow page loads. However, it lacks one vital feature โ€” an easy way to copy a code block to the clipboard. I decided to document my implementation using only vanilla JS since every blog post I found for this issue relied on jquery to parse the DOM, which is completely unnecessary at this point.

portfolio-site's People

Contributors

a-luna avatar

Watchers

 avatar

portfolio-site's Issues

Hugo: Add Copy-to-Clipboard Button to Code Blocks with Vanilla JS


title: 'Hugo: Add Copy-to-Clipboard Button to Code Blocks with Vanilla JS'
slug: "add-copy-button-to-code-blocks-hugo-chroma"
date: '2019-11-13'
aliases:
- /blog/hugo-chroma-syntax-highlight-copy-button/
- /blog/hugo-add-copy-button-to-code-blocks-chroma-highlight/
- /blog/hugo-chroma-add-copy-button-to-code-blocks/
menu_section: "blog"
categories: ['Hugo', 'Javascript']
summary: "Hugo includes a built-in syntax-highlighter called Chroma. Chroma is extremely fast since it is written in pure Go (like Hugo) and supports every language I can think of. Chroma's speed is especially important since syntax highlighters are notorious for causing slow page loads. However, it lacks one vital feature โ€” an easy way to copy a code block to the clipboard. I decided to document my implementation using only vanilla JS since every blog post I found for this issue relied on jquery to parse the DOM, which is completely unnecessary at this point."
resources:

  • name: cover
    src: images/cover.jpg
    params:
    credit: "Photo by Natalia Y on Unsplash"

Hugo includes a built-in syntax-highlighter called Chroma. Chroma is extremely fast since it is written in pure Go (like Hugo) and supports every language I can think of. Chroma's speed is especially important since syntax highlighters are notorious for causing slow page loads. However, it lacks one vital feature โ€” an easy way to copy a code block to the clipboard. I decided to document my implementation using only vanilla JS (every blog post I found for this issue relied on jquery to parse the DOM, which is completely unnecessary at this point).

The finished product can be seen/modified with the codepen below:

See the Pen Add Copy Button to Chroma (Hugo) Code Blocks by Aaron Luna (@a-luna) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

A quick search led me to this post on Danny Guo's blog. I used his example as my starting point but made several changes:

  • The "copy" button is placed within the code block rather than outside it.
  • Instead of polyfilling the Clipboard API, my implementation falls back to using document.execCommand("copy") if it is unsupported.
  • "Copy" buttons are only added to code elements that are generated by Chroma.

The Hugo highlight shortcode accepts a line-nos parameter. If line-nos is not specified or line-nos=inline, the rendered HTML has this structure:

<div class="highlight">
  <pre class="chroma">
    <code class="language-xxxx">
      (the code we wish to copy)
    </code>
  </pre>
</div>

If line-nos=table, the HTML is slightly more complicated:

<div class="highlight">
  <div class="chroma">
    <table class="lntable">
      <tbody>
        <tr>
          <td class="lntd">
            (line numbers are rendered here)
          </td>
          <td class="lntd">
            <pre class="chroma">
              <code class="language-xxxx">
                (the code we wish to copy)
              </code>
            </pre>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

I use the version with line numbers much more often than the version without, so it is important to me to support both. I decided to place the button inside the <div class="highlight"> element. Stacking elements on top of one another requires positioning and assigning z-index values, which you can see below along with the styling for the "copy" button:

.highlight-wrapper {
  display: block;
}

.highlight {
  position: relative;
  z-index: 0;
  padding: 0;
  margin: 0;
  border-radius: 4px;
}

.highlight > .chroma {
  color: #d0d0d0;
  background-color: #212121;
  position: static;
  z-index: 1;
  border-radius: 4px;
  padding: 10px;
}

.chroma .lntd:first-child {
  padding: 7px 7px 7px 10px;
  margin: 0;
}

.chroma .lntd:last-child {
  padding: 7px 10px 7px 7px;
  margin: 0;
}

.copy-code-button {
  position: absolute;
  z-index: 2;
  right: 0;
  top: 0;
  font-size: 13px;
  font-weight: 700;
  line-height: 14px;
  letter-spacing: 0.5px;
  width: 65px;
  color: #232326;
  background-color: #7f7f7f;
  border: 1.25px solid #232326;
  border-top-left-radius: 0;
  border-top-right-radius: 4px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 4px;
  white-space: nowrap;
  padding: 4px 4px 5px 4px;
  margin: 0 0 0 1px;
  cursor: pointer;
  opacity: 0.6;
}

.copy-code-button:hover,
.copy-code-button:focus,
.copy-code-button:active,
.copy-code-button:active:hover {
  color: #222225;
  background-color: #b3b3b3;
  opacity: 0.8;
}

.copyable-text-area {
  position: absolute;
  height: 0;
  z-index: -1;
  opacity: .01;
}

Did you notice that the CSS includes a selector for a highlight-wrapper class that is not present in the HTML structure generated by Chroma? We will create this element and append the positioned elements as a child node, then insert the wrapper into the DOM in place of the <div class="highlight"> element.

Similarly, the copyable-text-area class will be applied to a textarea element that will only exist if the Clipboard API is not available. This element will be added to the DOM and have it's value set to the innerText value of the code we wish to copy. After copying the text, the textarea element will be removed fom the DOM. The height: 0 and opacity: .01 stylings make it virtually invisible, and z-index: -1 places it behind the code block.

With that in mind, let's take a look at the JavaScript:

function createCopyButton(highlightDiv) {
  const button = document.createElement("button");
  button.className = "copy-code-button";
  button.type = "button";
  button.innerText = "Copy";
  button.addEventListener("click", () => copyCodeToClipboard(button, highlightDiv));
  addCopyButtonToDom(button, highlightDiv);
}

async function copyCodeToClipboard(button, highlightDiv) {
  const codeToCopy = highlightDiv.querySelector(":last-child > .chroma > code").innerText;
  try {
    result = await navigator.permissions.query({ name: "clipboard-write" });
    if (result.state == "granted" || result.state == "prompt") {
      await navigator.clipboard.writeText(codeToCopy);
    } else {
      copyCodeBlockExecCommand(codeToCopy, highlightDiv);
    }
  } catch (_) {
    copyCodeBlockExecCommand(codeToCopy, highlightDiv);
  }
  finally {
    codeWasCopied(button);
  }
}

function copyCodeBlockExecCommand(codeToCopy, highlightDiv) {
  const textArea = document.createElement("textArea");
  textArea.contentEditable = 'true'
  textArea.readOnly = 'false'
  textArea.className = "copyable-text-area";
  textArea.value = codeToCopy;
  highlightDiv.insertBefore(textArea, highlightDiv.firstChild);
  const range = document.createRange()
  range.selectNodeContents(textArea)
  const sel = window.getSelection()
  sel.removeAllRanges()
  sel.addRange(range)
  textArea.setSelectionRange(0, 999999)
  document.execCommand("copy");
  highlightDiv.removeChild(textArea);
}

function codeWasCopied(button) {
  button.blur();
  button.innerText = "Copied!";
  setTimeout(function() {
    button.innerText = "Copy";
  }, 2000);
}

function addCopyButtonToDom(button, highlightDiv) {
  highlightDiv.insertBefore(button, highlightDiv.firstChild);
  const wrapper = document.createElement("div");
  wrapper.className = "highlight-wrapper";
  highlightDiv.parentNode.insertBefore(wrapper, highlightDiv);
  wrapper.appendChild(highlightDiv);
}

document.querySelectorAll(".highlight")
  .forEach(highlightDiv => createCopyButton(highlightDiv));

So what is happening here? Whenever a page is loaded, all <div class="highlight"> elements are located and a "Copy" button is created for each. Then, the copyCodeToClipboard function is assigned as the event handler for the button's click event (Lines 2-6). Finally, some DOM manipulation is performed by calling addCopyButtonToDom. Let's examine how this function works:

  • Line 53: First, the "Copy" button is inserted into the DOM as the first child of the <div class="highlight"> element.

  • Line 54-55: We create a div element to act as a wrapper for the <div class="highlight"> element and assign the appropriate styling.

  • Line 56-57: Finally, the wrapper element is inserted into the DOM in the same location as the <div class="highlight"> element, and the <div class="highlight"> element is "wrapped" by calling appendChild.

When the user clicks a "Copy" button, the copyCodeToClipboard function is called. Since the logic that determines which copy function to use may not seem intuitive, let's go through it together:

  • Lines 13-14: Within the try/catch block, we first check if the clipboard-write permission has been granted.

  • Line 15: If the browser supports the Clipboard API and the clipboard-write permission has been granted, the text within the code block is copied to the clipboard by calling navigator.clipboard.writeText.

  • Line 17: If the browser supports the Clipboard API but the clipboard-write permission has not been granted, we call copyCodeBlockExecCommand.

  • Line 20: If the browser does not support the Clipboard API, an error will be raised and caught. Since this is an expected failure, the error is not re-thrown, and the same action that is performed when the browser supports the Clipboard API but the clipboard-write permission has not been granted is executed โ€” within the catch block we call copyCodeBlockExecCommand.

  • Line 23: Code within a finally block is called after either try/catch, regardless of result. Since the "copy" operation was invoked in either try/catch block, we call codeWasCopied which changes the button text to "Copied!". After two seconds the button text is changed back to "Copy".

  • Line 28-31: When the Clipboard API is unsupported/permission is not granted, we create a textarea element and assign the appropriate styling to make it hidden from the user but still available programmatically.

  • Line 32: We set the value of the textarea element to be equal the text the user wishes to copy.

  • Line 33: The textarea element is temporarily aded to the DOM next to the copy button.

  • Line 34-39: While testing my code, I found that it worked correctly on all browsers on desktop. However, on my iPhone the text wasn't being copied. I researched this issue and found the steps performed in these lines are needed.

  • Line 40-41: The textarea element is selected before calling document.execCommand("copy"), which copies the text we assigned to the textarea element to the clipboard. After doing so, the textarea element is removed from the DOM.

On this site, the JavaScript in the code block above is bundled with other js files and minified. If you'd like, you can verify the code and debug it using your browser's dev tools on any page that contains a code block (the easiest way would be to inspect the copy button, find the event listeners attached to it and add breakpoints to the method attached to the click handler). I hope this is helpful to you if you use Hugo and have run into the same problem, please leave any feedback/questions in the comments below!

An Introduction to Decorators in Python


title: 'An Introduction to Decorators in Python'
slug: intro-to-python-decorators
aliases:
- /blog/python-decorators-retry-timeout-exec-time/
- /blog/python-decorator-examples-timeout-retry/
date: '2020-02-27'
toc: true
menu_section: "blog"
categories:

  • Python
    summary: 'Decorators can be a daunting topic when first encountered. While the Zen of Python states "There should be one-- and preferably only one --obvious way to do it", there are many, equally valid ways to implement the same decorator. These different methods can be categorized as either function-based, class-based, or a hybrid of both. In this post I will explain the design and behavior of Python decorators and provide examples of decorators that I frequently use in my own code.'
    resources:
  • name: cover
    src: images/cover.jpg
    params:
    credit: "Photo by Yeh Xintong on Unsplash"

Introduction

Decorators can be a daunting topic when first encountered. While The Zen of Python states "There should be one-- and preferably only one --obvious way to do it", there are many, equally valid ways to implement the same decorator. These different methods can be categorized as either function-based, class-based, or a hybrid of both. In this post I will explain the design and behavior of Python decorators and provide examples of decorators that I frequently use in my own code.

What is a Decorator?

In Python, absolutely everything is an object, including functions. Since functions are objects, they can be passed as arguments to another function, they can be the return value of a function, and they can be assigned to a variable. If you understand these concepts then you have everything you need to understand decorators.

A decorator is any callable object that takes a function as an input parameter. I specifically said "callable object" rather than "function" since Python allows you to create other types of callable objects. This interesting language feature is what allows us to create class-based decorators, as we will see shortly.

A Simple Decorator

Decorators are wrappers that allow you to execute code before and after the "wrapped" (or "decorated") function is executed. By manually constructing a decorator function this "wrapping" effect can easily be demonstrated. Consider the decorators.basic module given below:

"""decorators.basic"""
def simple_decorator(function_to_decorate):
    def function_wrapper():

        print(f"Preparing to execute: {function_to_decorate.__name__}")
        function_to_decorate()
        print(f"Finished executing: {function_to_decorate.__name__}")

    return function_wrapper


def undecorated_function():
    print("I FORBID you from modifying how this function behaves!")
  • Line 2: simple_decorator accepts a function as a parameter, making it a decorator.

  • Line 3: function_wrapper (defined within simple_decorator) allows us to execute code before and after executing the wrapped function.

  • Line 5: This print statement will be executed before the wrapped function.

  • Line 6: The wrapped function is executed within function_wrapper.

  • Line 7: This print statement will be executed after the wrapped function

  • Line 9: This is probably the most confusing part. function_wrapper is the return value of the decorator function (simple_decorator). At this point, the wrapped function (function_to_decorate) HAS NOT been executed.

  • Line 12: We will use this function to demonstrate how simple_decorator works.

Open an interactive Python shell and execute undecorated_function() to see the behavior before applying any decorator. Next, we pass undecorated_function as a parameter to simple_decorator, and store the return value in decorated_function:

(venv) decorators $ python
Python 3.7.6 (default, Jan 19 2020, 06:08:58)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorators.basic import * 
>>> undecorated_function()
I FORBID you from modifying how this function behaves!
>>> decorated_function = simple_decorator(undecorated_function)

{{< info_box >}}
Please note the difference between calling a function (undecorated_function()) and passing a function as a parameter to another function (simple_decorator(undecorated_function)). If we had executed simple_decorator(undecorated_function()), we would not see the effect of the wrapper function. The behavior would be the same as when we executed undecorated_function().
{{< /info_box >}}

Finally, we execute decorated_function():

>>> decorated_function()
Preparing to execute: undecorated_function
I FORBID you from modifying how this function behaves!
Finished executing: undecorated_function

This is the result we expect after applying simple_decorator. However, wouldn't it be better if we could permanently alter the behavior of undecorated_function? We can easily do this if we replace the reference to undecorated_function with the function returned by simple_decorator:

>>> undecorated_function = simple_decorator(undecorated_function)
>>> undecorated_function()
Preparing to execute: undecorated_function
I FORBID you from modifying how this function behaves!
Finished executing: undecorated_function

This is exactly what happens when you decorate a function using Python's @decorator syntax. To reinforce this point, we can modify our code to use the normal decorator syntax. Note that we are applying the decorator to undecorated_function in Line 12:

"""decorators.basic"""
def simple_decorator(function_to_decorate):
    def function_wrapper():

        print(f"Preparing to execute: {function_to_decorate.__name__}")
        function_to_decorate()
        print(f"Finished executing: {function_to_decorate.__name__}")

    return function_wrapper


@simple_decorator
def undecorated_function():
    print("I FORBID you from modifying how this function behaves!")

Let's verify that the behavior of the function has been modified:

(venv) decorators $ python
Python 3.7.6 (default, Jan 19 2020, 06:08:58)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorators.basic import * 
>>> undecorated_function()
Preparing to execute: undecorated_function
I FORBID you from modifying how this function behaves!
Finished executing: undecorated_function

As you can see, @simple_decorator is just syntactic sugar for undecorated_function = simple_decorator(undecorated_function).

This type of decorator is nice, but what if our original function has one or more input parameters? It wouldn't be possible to provide values for these parameters with the current implementation of simple_decorator. What can we do to fix this?

Passing Arguments to the Wrapped Function

Fixing this is very easy. Remember, when we execute the "wrapped" function, we are really executing the function returned by the decorator function (function_wrapper in the previous example). Any values we provide to this function can be passed on to the wrapped function.

However, we need to accommodate all possible combinations of input parameters. We can do this by modifying function_wrapper to accept *args, **kwargs and passing them on to function_to_decorate. This has been implemented in a new module, decorators.better:

"""decorators.better"""
def a_better_decorator(function_to_decorate):
    def function_wrapper(*args, **kwargs):

        print(f"Preparing to execute: {function_to_decorate.__name__}")
        function_to_decorate(*args, **kwargs)
        print(f"Finished executing: {function_to_decorate.__name__}")

    return function_wrapper


@a_better_decorator
def greeting_anonymous():
    print("Hello anonymous website viewer!")


@a_better_decorator
def greeting_personal(name="Aaron"):
    print(f"Hello {name}!")

Let's test this version with the two decorated functions, greeting_anonymous and greeting_personal:

(venv) decorators $ python
Python 3.7.6 (default, Jan 19 2020, 06:08:58)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorators.better import * 
>>> greeting_anonymous()
Preparing to execute: greeting_anonymous
Hello anonymous website viewer!
Finished executing: greeting_anonymous
>>> greeting_personal()
Preparing to execute: greeting_personal
Hello Aaron!
Finished executing: greeting_personal
>>> greeting_personal(name="Jerry")
Preparing to execute: greeting_personal
Hello Jerry!
Finished executing: greeting_personal

This works exactly the way we need it to, the value we passed to greeting_personal is used because it was passed from function_wrapper to function_to_decorate. Also, we did not break any existing functionality since the function that does not take any input parameters (greeting_anonymous) is also decorated and behaves as expected.

Passing Arguments to the Decorator

The decorators we have created so far are certainly useful for many different scenarios, but they are also limited since there is no way to pass arguments to the decorator itself. How is this different than the last decorator we created? A decorator that accepts arguments would have the form shown below:

@foo(baz, fiz=buz)
def bar():
    ...

If you remember back to the beginning of this post, I mentioned that, in general, there are two different ways to implement Python decorators: function-based and class-based. The examples we have seen so far have all been function-based. Unless you need to create a decorator that accepts arguments, you should use these function-based designs since they are more readable and require less nesting/indentation than the equivalent class-based design.

However, when you need to pass arguments to a decorator, there isn't an advantage to using either the function-based or class-based design. Let's take a look at the function-based design first since it is a natural progression from the decorators we have already examined.

Function-based Design

The generic form of a function-based decorator that accepts arguments is given below:

"""decorators.function_based"""
def decorator_factory(arg1, arg2):
    def decorator_with_args(function_to_decorate):
        def function_wrapper(*args, **kwargs):

            print(f"First argument provided to decorator: {arg1}")
            function_to_decorate(*args, **kwargs)
            print(f"Second argument provided to decorator: {arg2}")

        return function_wrapper

    return decorator_with_args


@decorator_factory("foo", "bar")
def special_greeting(name="Dennis"):
    print(f"Allow me to give a very special welcome to {name}!")

In order to pass arguments to the decorator, we add another wrapper function, called decorator_factory (Line 2). I am calling it a factory because the return value is the actual function decorator (decorator_with_args is where function_to_decorate is passed in as an argument).

In order to understand how this works you need to realize that Line 15 (where we are applying the decorator to a function) is actually a function call to decorator_factory("foo", "bar") which returns decorator_with_args. decorator_with_args is the actual decorator but in order to pass the params "foo" and "bar" to the decorator, we had to call the factory method that creates the actual decorator which accepts the function special_greeting as an argument and wraps it.

Contrast this with the other decorators we have created: when a function is decorated with @simple_decorator or @a_better_decorator these are not function calls (notice that there are no parentheses after the decorator name).

Let's fire up the REPL and test this decorator. We will also verify that we are still able to use the decorated function as expected by passing arguments that modify its behavior:

(venv) decorators $ python
Python 3.7.6 (default, Jan 19 2020, 06:08:58)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorators.function_based import * 
>>> special_greeting()
First argument provided to decorator: foo
Allow me to give a very special welcome to Dennis!
Second argument provided to decorator: bar
>>> special_greeting(name="Harold")
First argument provided to decorator: foo
Allow me to give a very special welcome to Harold!
Second argument provided to decorator: bar

As you can see, the arguments that were passed into the decorator ("foo" and "bar") were used by the wrapper function, and the value passed into the decorated function ("Dennis"/"Harold") is also used when the wrapper function is executed.

Functions, Classes and Callables

Are you familiar with the built-in callable function? callable accepts a single argument and returns a bool value: True if the object provided appears to be callable and False if it does not. If you think that functions are the only "callable" that exists you might consider this function rather useless or unnecessary. However, classes are also callable since this is how new instances are created (e.g., object = MyObject()).

The call syntax, (...), can call functions or create class instances as we have just seen. But Python has a unique feature that objects other than functions can also be called. Adding the __call__ method to any class will make instances of that class callable. This allows us to create decorators that are implemented using classes.

Class-based Design

The same decorator can be implemented using a callable class instance:

"""decorators.class_based"""
class DecoratorFactory:
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2

    def __call__(self, function_to_decorate):
        def function_wrapper(*args, **kwargs):

            print(f"First argument provided to decorator: {self.arg1}")
            function_to_decorate(*args, **kwargs)
            print(f"Second argument provided to decorator: {self.arg2}")

        return function_wrapper


@DecoratorFactory("foo", "bar")
def special_greeting(name="Dennis"):
    print(f"Allow me to give a very special welcome to {name}!")

The main difference between the function-based and class-based designs is how the arguments passed to the decorator are handled. In the function-based approach, the arguments are available to function_wrapper as local variables. In the class-based design, the arguments are provided to the __init__ method and assigned to instance variables which can be accessed from function_wrapper.

We can confirm that this decorator behaves in exactly the same way as the function-based version:

(venv) decorators $ python
Python 3.7.6 (default, Jan 19 2020, 06:08:58)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorators.class_based import * 
>>> special_greeting()
First argument provided to decorator: foo
Allow me to give a very special welcome to Dennis!
Second argument provided to decorator: bar
>>> special_greeting(name="Hank")
First argument provided to decorator: foo
Allow me to give a very special welcome to Hank!
Second argument provided to decorator: bar

Which Design Is Better?

Is there any advantage to using either decorator design? In my opinion, the class-based design is flatter and easier to read, making it the more Pythonic choice. However, I acknowledge that the function-based design is more conventional since the idea of a callable object that is an instance of a class (rather than a function) is not what most people expect when they encounter the concept of decorators.

Other than that, there are no obvious benefits to choosing one design over the other. You should use the design that makes the most sense to you.

Always Use functools.wraps

I have intentionally left out something very important from these decorator examples. You should ALWAYS decorate function_wrapper (or whatever name is used in your application) with the functools.wraps decorator (located in the standard library's functools module). Why is this important? Consider the example below:

(venv) decorators $ python
Python 3.7.6 (default, Jan 19 2020, 06:08:58)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorators.class_based import * 
>>> import inspect
>>> special_greeting.__name__
'function_wrapper'
>>> inspect.signature(special_greeting)
<Signature (*args, **kwargs)>

When we inspect the name and signature of the special_greeting function, we instead receive the name and signature of the decorator that was applied to it. While confusing, it becomes even more of a headache if you need to debug this code. This is easily fixed with the functools.wraps decorator (Lines 2,11):

"""decorators.class_based"""
from functools import wraps


class DecoratorFactory:
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2

    def __call__(self, function_to_decorate):
        @wraps(function_to_decorate)
        def function_wrapper(*args, **kwargs):

            print(f"First argument provided to decorator: {self.arg1}")
            function_to_decorate(*args, **kwargs)
            print(f"Second argument provided to decorator: {self.arg2}")

        return function_wrapper


@DecoratorFactory("foo", "bar")
def special_greeting(name="Dennis"):
    print(f"Allow me to give a very special welcome to {name}!")

Now, if we inspect special_greeting we will see the correct name and signature:

(venv) decorators $ python
Python 3.7.6 (default, Jan 19 2020, 06:08:58)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorators.class_based import * 
>>> import inspect
>>> special_greeting.__name__
'special_greeting'
>>> inspect.signature(special_greeting)
<Signature (name='Dennis')>

{{< info_box >}}
Why did I not include the functools.wraps decorator in the previous examples? Since the point was to explain how decorators work, seeing a random decorator in the middle of everything would have been confusing and would have drawn attention away from the core concepts being demonstrated.
{{< /info_box >}}

Hopefully you have a better understanding of how decorators are designed and how they behave in Python. The remainder of this post will contain examples of decorators that I frequently use in my Python projects along with pytest functions that demonstrate the intended usage.

Example: Function Timeout

If you have ever written code that interacts with an outside service, you have probably encountered a situation where your program becomes stuck waiting for a response with no way to abort the function call. One way to get un-stuck is with the @timeout decorator:

"""decorators.timeout"""
from functools import wraps
from signal import signal, alarm, SIGALRM


def timeout(*, seconds=3, error_message="Call to function timed out!"):
    """Abort the wrapped function call after the specified number of seconds have elapsed."""

    def _handle_timeout(signum, frame):
        raise TimeoutError(error_message)

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            signal(SIGALRM, _handle_timeout)
            alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                alarm(0)
            return result

        return wrapper

    return decorator

{{< info_box >}}
The syntax used in Line 6 above prevents the user from specifying positional arguments with the @timeout decorator. This is done to ensure that the purpose of the decorator is clear to the reader based on its usage. Without the asterisk (*) in the function definition, the user could decorate a function with @timeout(3), rather than @timeout(seconds=3). The latter usage communicates the meaning of the value 3 in the context of the @timeout decorator. If you are unfamiliar with this syntax, you can find the background and justification for this feature in PEP 3102.
{{< /info_box >}}

It's very easy to use. Simply decorate any function with @timeout(seconds=X) and specify the number of seconds to wait before aborting the function call as shown below (Line 7). If the function completes before the specified number of seconds have elapsed, your program will continue executing. However if the function has not completed after the specified number of seconds have elapsed, a TimeoutError will be raised, aborting the function call.

In the simple test scenario below, the sleep function waits for two seconds when called. Since it is decorated with @timeout(seconds=1) a TimeoutError will be raised one second after it is called. The test_timeout function verifies that the correct error is raised (Lines 13-14).

"""tests.test_timeout"""
import time
import pytest
from decorators.timeout import timeout


@timeout(seconds=1)
def sleep():
    time.sleep(2)


def test_timeout():
    with pytest.raises(TimeoutError):
        sleep()

Example: Retry Function

The next example is similar to the @timeout decorator since both are designed to handle functions that are unreliable. The @retry decorator adds retry logic to the decorated function.

To use it, you specify a set of exceptions that can trigger a failed attempt, the number of failed attempts that can occur before aborting the function call (max_attempts), the number of seconds to delay after each failed attempt before trying again (delay) and an optional handler method to be called whenever an exception is raised (for logging, etc).

The actual decorator definition begins on Line 26 below. Before that, the custom Exception RetryLimitExceededError is defined (Lines 6-13). This is the exception raised after max_attempts to call the function have failed. The handle_failed_attempt function (Lines 16-23) is provided as an example of what could be provided to the @retry decorator's on_failure parameter.

"""decorators.retry"""
from functools import wraps
from time import sleep


class RetryLimitExceededError(Exception):
    """Custom error raised by retry decorator when max_attempts have failed."""

    def __init__(self, func, max_attempts):
        message = (
            f"Retry limit exceeded! (function: {func.__name__}, max attempts: {max_attempts})"
        )
        super().__init__(message)


def handle_failed_attempt(func, remaining, ex, delay):
    """Example function that could be supplied to on_failure attribute of retry decorator."""
    message = (
        f"Function name: {func.__name__}\n"
        f"Error: {repr(ex)}\n"
        f"{remaining} attempts remaining, retrying in {delay} seconds..."
    )
    print(message)


def retry(*, max_attempts=2, delay=1, exceptions=(Exception,), on_failure=None):
    """Retry the wrapped function when an exception is raised until max_attempts have failed."""

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for remaining in reversed(range(max_attempts)):
                try:
                    return func(*args, **kwargs)
                except exceptions as ex:
                    if remaining > 0:
                        if on_failure:
                            on_failure(func, remaining, ex, delay)
                        sleep(delay)
                    else:
                        raise RetryLimitExceededError(func, max_attempts) from ex
                else:
                    break

        return wrapper

    return decorator

We can use the @timeout decorator to demonstrate and test the @retry decorator. Since we need to know the type of Exception that we expect to occur when we call the decorated function, we specify exceptions=(TimeoutError,) since this is the error raised by the @timeout decorator.

With the code below, we will attempt to call retry_with_timeout a maximum of two times. Since the only thing this function does is wait for two seconds and we have decorated it with @timeout(seconds=1), calling it will always raise a TimeoutError. Therefore, after the second failed attempt, the @retry decorator will raise a RetryLimitExceededError.

Finally, the test_retry_with_timeout function verifies that the RetryLimitExceededError is in fact raised after calling the retry_with_timeout function.

"""tests.test_retry"""
import time

import pytest

from decorators.retry import retry, RetryLimitExceededError
from decorators.timeout import timeout


@retry(max_attempts=2, delay=1, exceptions=(TimeoutError,))
@timeout(seconds=1)
def retry_with_timeout():
    time.sleep(2)


def test_retry_with_timeout():
    with pytest.raises(RetryLimitExceededError):
        retry_with_timeout()

Example: Log Call Signature and Execution Time

The most common application of decorators might be logging. It's easy to see why, having the ability to run code immediately before and after a function is called allows you to report information and capture metrics.

This decorator uses the class-based design, and allows the user to provide a custom logger. If none is provided, a logger will be created based on the module that contains the wrapped function.

Whenever the function is called, the @LogCall() decorator adds an info level entry the the log with the following data:

  • Timestamp when the function was called
  • function name and values of all arguments provided to the function (i.e. the call signature)
  • Time elapsed while executing the function
"""decorators.log_call"""
import inspect
import logging
from datetime import datetime
from functools import wraps

DT_NAIVE = "%Y-%m-%d %I:%M:%S %p"


class LogCall:
    """Log call signature and execution time of decorated function."""

    def __init__(self, logger=None):
        self.logger = logger

    def __call__(self, func):
        if not self.logger:
            logging.basicConfig()
            self.logger = logging.getLogger(func.__module__)
            self.logger.setLevel(logging.INFO)

        @wraps(func)
        def wrapper(*args, **kwargs):
            func_call_args = get_function_call_args(func, *args, **kwargs)
            exec_start = datetime.now()
            result = func(*args, **kwargs)
            exec_finish = datetime.now()
            exec_time = format_timedelta_str(exec_finish - exec_start)
            exec_start_str = exec_start.strftime(DT_NAIVE)
            self.logger.info(f"{exec_start_str} | {func_call_args} | {exec_time}")
            return result

        def get_function_call_args(func, *args, **kwargs):
            """Return a string containing function name and list of all argument names/values."""
            func_args = inspect.signature(func).bind(*args, **kwargs)
            func_args.apply_defaults()
            func_args_str = ", ".join(f"{arg}={val}" for arg, val in func_args.arguments.items())
            return f"{func.__name__}({func_args_str})"

        def format_timedelta_str(td):
            """Convert timedelta to an easy-to-read string value."""
            (milliseconds, microseconds) = divmod(td.microseconds, 1000)
            (minutes, seconds) = divmod(td.seconds, 60)
            (hours, minutes) = divmod(minutes, 60)
            if td.days > 0:
                return f"{td.days}d {hours:.0f}h {minutes:.0f}m {seconds}s"
            if hours > 0:
                return f"{hours:.0f}h {minutes:.0f}m {seconds}s"
            if minutes > 0:
                return f"{minutes:.0f}m {seconds}s"
            if td.seconds > 0:
                return f"{td.seconds}s {milliseconds:.0f}ms"
            if milliseconds > 0:
                return f"{milliseconds}ms"
            return f"{td.microseconds}us"

        return wrapper

The code below tests the @LogCall() decorator with a custom logger and with the default logger. With the default logger, we expect the name of the logger to be the name of the module containing the decorated function, tests.test_log_call (Line 30). When a custom logger is provided, we expect the name to match the value we specified when the logger was created, custom_log (Lines 10, 38).

A nice feature of this decorator is that the function call signature contains the names and values of all keyword arguments, even if a default value was used or if the name was not given when the call occurred. For example, the call to the decorated function in Line 28 is save_values("Aaron", "Charlie", "Ollie"), but the call signature that is logged contains the names of all three arguments, save_values(a=Aaron, b=Charlie, c=Ollie) (Line 32).

Similarly, the call to the decorated function in Line 36 is rand_time(max=4, add_random=True), which only provides two arguments. The call signature that is logged includes the default value of the missing argument, rand_time(min=1, max=4, add_random=True) (Line 40).

"""tests.test_log_call"""
import time
import logging
from random import randint

from decorators.log_call import LogCall


logging.basicConfig()
log = logging.getLogger("custom_log")
log.setLevel(logging.INFO)
log.info("logging started")


@LogCall()
def save_values(a, b, c):
    pass


@LogCall(log)
def rand_time(min=1, max=3, add_random=False):
    time.sleep(randint(min, max))
    if add_random:
        time.sleep(randint(100, 500) / 1000.0)


def test_default_logger(caplog):
    save_values("Aaron", "Charlie", "Ollie")
    logger, level, message = caplog.record_tuples[-1]
    assert logger == "tests.test_log_call"
    assert level == logging.INFO
    assert "save_values(a=Aaron, b=Charlie, c=Ollie)" in message


def test_custom_logger(caplog):
    rand_time(max=4, add_random=True)
    logger, level, message = caplog.record_tuples[-1]
    assert logger == "custom_log"
    assert level == logging.INFO
    assert "rand_time(min=1, max=4, add_random=True)" in message

If we run pytest for these decorator examples, all of the tests pass:

(venv) decorators $ pytest tests/test_*
================================================================= test session starts ==================================================================
platform darwin -- Python 3.7.6, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /Users/aaronluna/Desktop/vigorish/venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/aaronluna/Desktop/vigorish, inifile: pytest.ini
plugins: clarity-0.3.0a0, black-0.3.8, mypy-0.5.0, dotenv-0.4.0, flake8-1.0.4, cov-2.8.1
collected 10 items

tests/test_log_call.py::FLAKE8 PASSED                                                                                                            [ 10%]
tests/test_log_call.py::BLACK PASSED                                                                                                             [ 20%]
tests/test_log_call.py::test_default_logger PASSED                                                                                               [ 30%]
tests/test_log_call.py::test_custom_logger PASSED                                                                                                [ 40%]
tests/test_retry.py::FLAKE8 PASSED                                                                                                               [ 50%]
tests/test_retry.py::BLACK PASSED                                                                                                                [ 60%]
tests/test_retry.py::test_retry_with_timeout PASSED                                                                                              [ 70%]
tests/test_timeout.py::FLAKE8 PASSED                                                                                                             [ 80%]
tests/test_timeout.py::BLACK PASSED                                                                                                              [ 90%]
tests/test_timeout.py::test_timeout PASSED                                                                                                       [100%]

================================================================== 10 passed in 6.63s ==================================================================

Summary

If you would like to download all or some of the code from this post, you can easily do so from the Github gist linked below:

I hope this introduction to decorators in Python was helpful and easy to understand. If you have any questions, criticism or feedback please leave a comment. Thanks!

AWS Free Usage Tier: An Incredibly Useful and Generous Offering


title: "AWS Free Usage Tier: An Incredibly Useful and Generous Offering"
slug: "aws-free-usage-tier"
aliases:
- /2018/01/13/aws-free-usage-tier/
date: "2018-01-13"
menu_section: "blog"
categories: ["AWS"]
summary: ""
resources:

  • name: cover
    src: images/cover.jpg
    params:
    credit: "Photo by Jake Blucker on Unsplash"

For the past 2 months, I have been learning Linux server administration, PHP/JavaScript and other skills such as DevOps tools/processes. By far, the most valuable resource I have used to develop these skills is the Amazon Web Services (AWS) Free Usage Tier that is offered to anyone for a period of one year. Well, anyone with a credit card to be more precise.

{{

}}

AWS comprises an overwhelming number of products and services. Take a look at the list here. Besides the expected categories like "compute", "database", or "developer tools", the free tier also provides access to many services that I was unaware of:

However, "free" in this sense does not mean unlimited. Each service has a defined limit which when exceeded will begin billing at the service's normal rate (the limit for most services resets each month). The overage is invoiced monthly and charged to the credit card you provided at signup.

Many of the products remain "free" after the 1 year term is over. The most useful of which (to me) is the CodeCommit service. I built and deployed a Gogs EC2 instance which I planned to use as a private code repository for my personal projects. I quickly took it down when I learned that AWS offers the service completely free-to-use, forever, for up to 5 user accounts. There are monthly limits on the size of your repositories and frequency of git requests, but these will never be reached by my usage.

Amazon provides this free tier to allow users to gain experience with the AWS platform, with the obvious goal of encouraging adoption of their products in large-scale applications and organizations the user belongs to. On each AWS product page, Amazon boasts of such customers: the WaPo utilizes the Comprehend service, NASA has leveraged Lex and Zillow uses Lambda.

Just as impresively, I used multiple Amazon services to host this website. I registered my domain and configured the associated record sets through Route 53 (This product does not have a free offering). I installed the Wordpress core on a t2.micro EC2 instance and deployed the Wordpress database on an RDS instance. I store site assets such as image files in a S3 bucket. My RDS instance can only communicate with the Wordpress EC2 server and is isolated from any outside web traffic thanks to custom subnet configurationsmade in my Virtual Private Cloud (VPC).

In my next post, I will give recommendations on how to manage billing for your AWS account and how to avoid going over the free tier limits when hosting a website.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.