back to indexnbdev tutorial -- zero to published project in 90 minutes
Chapters
0:0 Welcome
4:39 Turn a github repo into a nbdev repo
6:48 What are nbdev commands?
8:17 How to find out the docs of a nbdev command
8:37 What does nbdev_new give us?
10:54 How should you name ipynbs for your library
15:32 What does the heading 1 and heading 2 do?
18:2 Create a Card module
19:1 Creating suits
21:23 Creating a class
26:54 How to overwrite the __str__ and __repr__
29:3 How to write docs for input argument
33:13 Create tests for the class
37:6 How to define equality
38:32 How to define a function outside of a class
42:18 How to export your Card py file with nbdev_export
48:33 How to preview your documentation
55:2 How to do local test
56:52 How to do debugging in real life
60:51 Which cells should be exported with #|export
62:9 Creating Deck module
66:41 How to overwrite the __len__ and __contains__
75:22 Automatic links
78:48 Creating a function
81:50 Pushing back to github
87:21 run nbdev_docs to put the homepage inside README.md
88:47 Release Your Library
90:16 How nbdev makes PR easier for all
00:00:00.880 |
This is a tutorial where we're going to be talking about 00:00:13.280 |
more and different and interesting ways to develop software and I am here with Hamel. 00:00:18.960 |
Do you want to say hi Hamel? Hi everybody. Hamel's joining us from where are you Hamel? 00:00:25.920 |
I'm actually at the RStudio conference in Washington DC. Tomorrow we're doing the launch of 00:00:32.480 |
MBDev because it's built on Cordo which is so I'm here in a hotel room. 00:00:40.080 |
Awesome thanks for joining us in the midst of your conference. All right let me share my screen here. 00:00:55.760 |
Great so as Hamel mentioned we're going to be using MBDev today which works with Cordo 00:01:05.360 |
to take a bunch of Jupyter notebooks and to turn it into a complete software package. 00:01:21.920 |
So we're going to walk through how we would go about doing that from scratch. 00:01:26.240 |
It's actually much more exciting than that but go on get us excited. 00:01:31.520 |
Not only will we create a software package but we'll show you how you can get a documentation 00:01:39.920 |
site, a beautiful documentation site that's searchable for free. You'll get CI for free. 00:01:46.400 |
You'll get an amazing way to do unit tests and testing all within the same context. It's something 00:01:53.920 |
that has made me more productive at least 10 times more productive while building all kinds 00:02:02.320 |
of different software projects. I think you know I've been using MBDev for about four years now 00:02:08.400 |
or three years and I've done a lot of different types of or made a lot of different types of 00:02:14.400 |
software. Everything from CLI apps to API clients to you know I've worked on the extensions of the 00:02:23.680 |
Python programming language with Jeremy and a bunch of other things. It's interesting how many 00:02:30.320 |
different use cases that it's a really good fit for. I think it's wonderful so I'm really excited 00:02:39.680 |
to show it to everybody. What about you Jeremy? I think the big thing for me is I don't really 00:02:48.000 |
enjoy writing software very much but I'm not using MBDev now because I don't get as much get into 00:02:55.440 |
that flow state which is such a pleasure. Using a notebook because I'm doing exploratory programming 00:03:02.560 |
I'm really in the zone the whole time. I very rarely have mysterious bugs because 00:03:11.040 |
everything along the way I've built an exploratory way. I know exactly how it works and it's very 00:03:16.480 |
easy to fix any problems so I'm kind of in this continuous zone of productivity which feels 00:03:23.760 |
enjoyable you know and I've had various ways of trying to achieve something like that before 00:03:29.680 |
MBDev existed but I never had the same experience that this gives me. 00:03:33.440 |
The other thing as you mentioned is like you get all this stuff for free you know so the fact that 00:03:45.680 |
I can quickly whip out in a couple of hours a complete project with continuous integration 00:03:50.160 |
tests documentation PIP installers all that stuff is pretty cool. So that's what we're going to 00:03:56.640 |
build today just a kind of a fun little sample project it's not going to do anything interesting 00:04:02.800 |
this one it's going to be based on this book Think Python by Alan Downey which is a really great 00:04:09.840 |
book and we're going to build a deck of cards so they're basically going to be inspired by 00:04:15.680 |
the deck of cards idea that comes from his book to do a bit of OO programming in Python and we're 00:04:21.680 |
going to end up with a documentation site a conda package a PyPy package tests and continuous 00:04:29.920 |
integration which before this video is finished. Am I missing anything Hamel? Is that what we're 00:04:35.680 |
going to have? Yeah yeah we can have all that. Great so the starting point for creating an MBDev 00:04:43.760 |
package is to create a repository the smoothest path is generally to use GitHub it's not strictly 00:04:53.280 |
required so here we are on GitHub I'll go ahead and create a repository and call it 00:05:02.800 |
MBDevCards for example give it a description okay 00:05:22.640 |
give it a patchy license create the repo there it is okay so that's step one so what we're going 00:05:34.960 |
to do now is we're going to clone this repo and turn it into an MPDev repository 00:05:41.760 |
so I'll click the copy button I'm going to head over to my terminal 00:05:57.840 |
that repo MBDevCards and this is assuming that I've already got MBDev installed so to install MBDev 00:06:07.840 |
you can follow the directions on the home page 00:06:14.640 |
so there's a written tutorial and just above it you'll see the pip install or conda install 00:06:29.840 |
commands you can use MBDev is very very lightweight it has very few dependencies 00:06:39.280 |
dependencies are basically fast core and exec nb that's about it you don't even need Jupyter or 00:06:44.320 |
anything to to run it obviously to author stuff you'll need Jupyter but yeah it's a very lightweight 00:06:49.760 |
library so once you've got it installed if you type MBDev underscore and hit tab you'll find that 00:06:56.960 |
there's a bunch of command line tools that it's installed for you and so you can get a list of 00:07:02.320 |
them here on the nbdev main docs page and the one we're going to use is nbdev new 00:07:09.520 |
and you can also see a list of them anytime if you do nbdev help in the terminal as well 00:07:17.760 |
at the you know you get a short description along them okay and you know this is actually a good 00:07:26.640 |
example Hamill of how we don't have to do anything to keep our documentation up to date because our 00:07:35.280 |
documentation being an nbdev is written in nbdev and so the documentation actually uses exclamation 00:07:40.960 |
mark that means run a shell command in Jupyter followed by that shell command and the output 00:07:46.000 |
here so the output in our documentation will always be up to date because it's running the 00:07:51.920 |
actual code so this is this is huge because like the way that most people create documentation is 00:07:58.480 |
they copy and paste code into markdown and they copy and paste the output into markdown and it 00:08:03.120 |
becomes stale it becomes a big headache and that's why nobody writes documentation but this is why 00:08:08.720 |
nbdev is for lazy people like me i'm very lazy you know not not prepared to do things twice 00:08:16.000 |
okay so nbdev new so you can pass minus h to any nbdev command to find out how to use it 00:08:27.120 |
in this case it's very very simple you just this got no command line arguments so you can just 00:08:33.520 |
go ahead and just run nbdev here so as you can see here when i run nbdev new it 00:08:40.800 |
figures out all the details of my repo and it creates a file called settings.any and settings.any 00:08:50.560 |
is your your home for all the stuff that you need for your app or your library and the 00:09:01.840 |
the neat thing about this for a lazy person like me is that you 00:09:09.280 |
only have to put this stuff in one place at one time like the version number and your details and 00:09:15.280 |
so forth yeah it means that you don't have to worry about putting these things in multiple 00:09:24.240 |
places for your documentation for your pi pi install or whatever it's all going to come from 00:09:28.640 |
one place and you don't have to worry about it that is great otherwise you know like package 00:09:35.440 |
management has so much boilerplate it's overwhelming and this makes it to where you can 00:09:41.520 |
actually do it because it like it stays sane. 00:09:44.640 |
So the next thing we're going to do then is create um 00:09:59.040 |
let's see what we've got here so it's given us an oocore notebook an index notebook 00:10:11.920 |
and some style sheet info so we'll learn about what all these things are in a moment 00:10:20.080 |
so let's start by opening up the the home page that you're going to be using 00:10:27.440 |
um so index.i.pi.nb is going to become your home page 00:10:32.480 |
as you can see and oocore is the home page for what home page for the docs yeah home page for 00:10:42.640 |
your documentation website exactly and also become like the the first thing that you create 00:10:49.440 |
for your library and we're only really well we're going to use a couple of modules so that's 00:10:53.600 |
that's fine um in fact um i think for this one we're not necessarily going to have one called 00:10:59.760 |
core i think we're going to have cards and a deck we're going to put them just for 00:11:05.600 |
explanation into two different modules so maybe we'll create cards first 00:11:09.040 |
so maybe we'll rename this to oocards or card yeah card you want to say something about the 00:11:20.960 |
oo in front does that mean anything to you i mean i like yeah i think it's helpful to have 00:11:26.320 |
things have some ordering like the order in which things are designed to be a read and be built like 00:11:35.120 |
not so much how the software builds it but how you built it so that you know the nice thing about 00:11:39.920 |
this kind of literate programming approach is that because the documentation is the source 00:11:44.720 |
that's because because the source code uh notebooks somebody who wants to get up to speed on your 00:11:49.280 |
library is reading notebooks you know um in fact you know nb dev is a good example of that so if 00:11:55.920 |
we go to the github repo for nb dev and we're like okay well let's learn about how this software is 00:12:01.440 |
written i can start on the very first one and i know that's that's what i need to start reading 00:12:08.000 |
to start understanding how this is written and um you know i can start okay so here's what the 00:12:15.280 |
settings that any is and here's where it comes from in fact even better might be if we look at 00:12:20.880 |
exec nb which is the um library that we've written to help work uh with notebooks 00:12:32.960 |
because you can really see if we start at the very start here 00:12:40.480 |
like literally you know it's prose because like this was my expiration when i started writing 00:12:45.920 |
this is like what's a notebook so i started opening them and reading them and printing out 00:12:49.200 |
what's in them so therefore when you know when you the reader start reading my code 00:12:54.240 |
you're following me on my journey of understanding yeah what what's what's going on at every step and 00:13:02.080 |
then you can see when i've written a function you can understand why i've written that function 00:13:06.800 |
because i've just explored up to a point where we can see that's the function i now need 00:13:10.720 |
um so for example nb to dict is basically doing things that i've just done step by step in prose 00:13:17.600 |
so yeah that's the main thing you know the ordering of the file names is also used by default to order 00:13:26.880 |
the table of contents in your documentation so that's another good reason to have it 00:13:31.120 |
make some sense so that's the only reason i'm using those numbers there 00:13:40.400 |
a deck of cards so we'll call this 101 deck so that's going to be a home page that's going 00:13:53.760 |
to be a module for cards this will be a module for decks um 00:13:59.520 |
in real life i would probably be putting these in the same module because they're not going to be 00:14:05.360 |
very big but this is just good for demo i think um so each notebook is going to produce one .py file 00:14:14.960 |
one module uh so what do you want that module to be called this is going to go up here so 00:14:22.640 |
there's you'll see there's some special comments 00:14:30.240 |
here in the notebook they always start with hash pipe and if you've ever used quarter 00:14:35.760 |
before this will look very familiar because in quarter it's exactly the same 00:14:40.720 |
you can see special comments with hash pipe here as well 00:14:55.360 |
so these comments that are used in quarter and nbdev are used to tell quarter or nbdev 00:15:00.800 |
something about this code so for example we'll be learning about showdoc in a moment and we have to 00:15:08.560 |
import showdoc but the fact that we're doing that is something that the reader of your documentation 00:15:13.680 |
doesn't care about so we hide this from the documentation that's built so this special one 00:15:20.880 |
here called default x this is the default export this is what we're going to export 00:15:25.120 |
symbols here into what module we're going to import it into a module called card.py 00:15:30.480 |
this is marked down in our notebook and so this is where we can start typing things that we want 00:15:37.600 |
to appear both for the reader of our source code and also it's going to end up in our documentation 00:15:53.760 |
a simple api for creating and using playing cards so this is the description 00:16:12.400 |
okay it might be worth the point that this having this header one in this note block is a kind of 00:16:23.040 |
an nbdev shortcut and what happens is this becomes the title of your page and that that quote block 00:16:31.440 |
becomes the description of your page when it renders. Yeah maybe a good way to understand 00:16:37.920 |
how this works is to look at a library so let's take exec nb again and open up one of its notebooks 00:16:55.440 |
its rendered documentation and so you'll see here we've got nbio and shell and here in the 00:17:05.280 |
documentation here they are nbio and shell and if we look at the notebook 00:17:10.160 |
you can see here is the header one so that's become the title and the contents 00:17:20.400 |
and the description here has come from the description 00:17:25.600 |
and that's used in things like the metadata of the page as well 00:17:33.380 |
you can see the title for example gets built from that automatically 00:17:40.320 |
so that's a good way to kind of understand how to use nbdev and how your choices make things appear 00:17:50.080 |
is by looking at the sample and then you'll see each of the second level headings ends up as 00:17:56.000 |
and third level headings ends up as in the table of contents here 00:17:59.360 |
so um i kind of like to think about well how do i how do i want my um 00:18:11.200 |
how do i want this to behave you know and so we're basically going to be we're going 00:18:17.760 |
to be creating a playing card um so i'm going to want to have some kind of like 00:18:21.840 |
something which i could do like create a card passing it like a numeric suit 00:18:36.080 |
create uh you know a list of suits for example 00:18:59.680 |
um so we've now got a bunch of suits so you could like say suits one for example 00:19:23.680 |
hmm yeah something about python's unicode handling maybe 00:19:39.680 |
my computer it actually visually looks different than this i don't get colored like i don't yeah 00:19:48.000 |
oh okay like yeah i don't ever see these colors or anything on my computer when on the same 00:19:53.280 |
notebook so this is where like nb dev is really helpful right because you know i'm not going to 00:20:01.520 |
end up with some weird bug deep in my code because i'm exploring as i go um i thought you could like 00:20:07.520 |
split things hmm okay so i think what we'll do instead since we discovered that doesn't work 00:20:16.240 |
which i think just shows my ignorance about how python uses unicode 00:20:21.760 |
let's put them create a list of strings instead 00:20:38.400 |
my guess is maybe i think like some emojis are actually like consists of multiple kind of 00:20:47.440 |
code points or something i don't quite remember the terminology and it kind of ends up changing 00:20:52.080 |
the color like you know like a flag or something yeah um 00:21:06.240 |
so then we're going to have all the different card ranks 00:21:17.520 |
and so there isn't really a rank zero so there's a bit of placeholder there 00:21:24.960 |
and so this this code is kind of loosely based on alan downey's book 00:21:37.440 |
so cool so um so if we want to create a class that represents a card we'll say we want a class 00:21:55.040 |
python calls the dunder init when you create an object of a class and so we're going to be passing 00:22:04.320 |
in some suit and some rank and so then we'll just be setting self dot dot suit 00:22:15.680 |
just to store it away basically and self dot rank 00:22:20.240 |
we'll set them equal to the suit and the rank now i should mention like um 00:22:30.000 |
as you well know hamill the way i the way i write 00:22:33.680 |
code is different to like the way most people write python code and um 00:22:42.160 |
and in particular there's a lot of like specific recommendations in how to write 00:22:48.560 |
python code in a document called pep 8 um which is kind of like a default style guide for python code 00:22:57.200 |
um yeah folks who are working in an organization that uses pep 8 they don't take my particular 00:23:02.720 |
approach to coding as a role model um i will say though that i've been coding for 40 years now 00:23:10.560 |
and coding nearly every day for 30 years um and my particular approach to coding is not 00:23:19.040 |
random there's a particular reason for it um which i've documented on the fast ai coding style 00:23:25.680 |
and it based on kind of many decades of work from people much smarter than me 00:23:30.240 |
particularly ken iverson the cheering award winner um i just want to say yeah i like my 00:23:36.960 |
way of coding and that's what i'm going to show here um but if you're um yeah working in an 00:23:43.680 |
organization that uses python in a more traditional way you should go with your own organization's 00:23:49.840 |
coding style the style that i've developed uh as i say it's partly based on years of research 00:23:57.600 |
from other languages decades of research it's also partly based on experience with um exploratory 00:24:02.880 |
and literate programming in particular so it more closely follows the kind of style you would see 00:24:08.720 |
from lisp or apl or julia programmers or f sharp programmers like programming languages that 00:24:16.400 |
where working in the repel working in an interactive and exploratory way is more a part of the 00:24:20.880 |
the culture and toolkit of that um yeah of that environment can i say something about this 00:24:29.040 |
please so you know i think it's uh uh important if you are interested in nbdev to approach it 00:24:37.040 |
with the right mindset and i think uh one mindset that's been helpful for a lot of people is to 00:24:44.960 |
you know look at and the things that we're showing you today as a dialect of python because not only 00:24:51.760 |
are we you know going to show you uh you know this way of developing code in a notebook but there's 00:24:58.880 |
also uh you know there are some extensions to the python programming language that jeremy has made 00:25:05.200 |
that that kind of enhance the repel experience and so if you think of it as a dialect then you can 00:25:11.440 |
kind of open up your mind to you know a different different ways of working and also different 00:25:16.960 |
conventions um so i think that's really important yeah makes sense so while you're talking i was 00:25:25.600 |
just starting to fill in the kind of like information i would want to be passing on to a 00:25:37.600 |
that's great i think this is like really is this is super powerful for many reasons like 00:25:41.920 |
often i find that when i'm writing the documentation like this um you know i discover 00:25:48.960 |
bugs in my code or i discover clunky things in my code like hey this is very hard to explain 00:25:53.680 |
and i end up refactoring it because of that yeah definitely 00:26:09.120 |
okay so try to explain in our docs as we go why things are as they are 00:26:38.240 |
okay so there's a nice right so now we can create a card 00:26:53.360 |
um so that's not very helpful um and so that's the that's the default representation 00:27:06.480 |
so we can override that using dunder str which is a dunder str which is the python way of saying 00:27:14.960 |
this is how i want to stringify my object and i think a simple way to do this would be just 00:27:22.720 |
to use an f string and say this is like a first of all the rank so that would be the self dot rank 00:27:32.400 |
and then we would want to look that up in all the ranks 00:27:37.680 |
and then we'd want to do something similar for suits 00:27:49.680 |
okay and so that's what we're going to see if we print it 00:27:59.040 |
like so now as you'll see it's a little bit different to how it's represented in a notebook 00:28:03.680 |
so quite often i like my notebook representation to be the same as how it's printed it's an easy 00:28:08.720 |
way to do that is by default jupiter will use the dunder repra method to return the 00:28:17.360 |
representation in the notebook so if i just say i want this to be the same as the string version 00:28:28.800 |
so we can say uh so we can add some documentation here 00:29:12.720 |
is an example of creating and displaying a card and for the attentive people that are really 00:29:22.720 |
playing close attention these back ticks that jeremy put in his doc string just keep that in 00:29:27.200 |
the back of your mind that's actually doing something special and you'll see that yeah i 00:29:31.120 |
mean we can look at it now so what's going to happen automatically is this is going to be 00:29:34.480 |
turned into documentation so for example if we look at this one that's a good example so here's 00:29:43.680 |
a function called dict2nb you can see here that it's created some documentation and in fact we 00:29:58.720 |
could make the documentation nicer well let's take a look at it first 00:30:01.760 |
so there's the documentation it's going to auto generate it's going to call this function called 00:30:08.720 |
showdoc you actually don't have to put it in here manually it'll do it automatically when we build 00:30:12.400 |
the docs but you can kind of get a bit of a preview something which i think is nice to do is to give 00:30:18.160 |
each parameters and documentation so to do that you can use something we invented called documents 00:30:26.560 |
and this works like so you basically put a comment after a parameter and say and give it a comment so 00:30:35.120 |
an index into suits and so that's kind of nice because i can now make my doc string a bit simpler 00:30:45.680 |
and in fact i don't need to say passing in rank and suit because you can already 00:30:53.280 |
already see that they're right here so really at this point we can strip it all the way back 00:30:57.680 |
you know and having like oh in my opinion having 00:31:05.120 |
over lever verbose documentation isn't a good idea if you have more information than needed 00:31:10.320 |
then you know it's it's distracting to the reader you want the right amount of information 00:31:16.240 |
so at this point you can see here this is how it's going to be represented 00:31:21.360 |
that's the exact information i want very clear and so the other thing we might then do is say well 00:31:26.160 |
what what what type is expected okay so that way you don't have to include that in the doc string 00:31:33.040 |
because again when we spit out this show doc it's going to show me those types both in the table 00:31:40.080 |
and also in the signature so this again it's me being lazy i don't want to include any more 00:31:46.800 |
information than i and i have to as i say we don't actually need this here it'll be auto added for us 00:31:53.360 |
so after a while you kind of get used to what things look like so you don't need it but if 00:31:58.480 |
you do add it it's fine it will say that you've added it it won't add it twice 00:32:02.000 |
an advanced feature we won't necessarily be discussing today is you know you can 00:32:09.440 |
document other code bases with show doc yeah you know there's another reason you might want to 00:32:15.520 |
use it exactly so you might create docs for something that you've written without nbdev 00:32:21.600 |
okay docs for somebody else's library and that would be done by using show doc and importing 00:32:26.640 |
their library so for example if we wanted to you know document something from exec nb for example 00:32:36.880 |
i could import something like that thing we were just looking at 00:32:47.920 |
you know and start writing some markdown prose and also add wherever i wanted to 00:32:58.000 |
the actual documentation and this bit here this header show doc blah is not going to appear the 00:33:04.160 |
only thing that appears in the documentation is the markdown output okay so i think you know in 00:33:16.960 |
general we probably want to be able to recognize you know when when cards are the same or when 00:33:23.200 |
they're less than or greater than some other card so what i kind of like to do for that is 00:33:32.720 |
i kind of like creating tests to check that it's working correctly you can either create them 00:33:38.160 |
before or after it doesn't really matter too much with exploratory programming 00:34:00.160 |
so i would kind of be saying things so you can import 00:34:03.920 |
some basic testing functionality from fast core 00:34:09.680 |
and you know again this approach of importing wildcards it's like not the normal approach in 00:34:18.480 |
a lot of python libraries but for exploratory programming it works pretty well it's certainly 00:34:25.040 |
not unheard of like for example the python standard library itself has a really famous library called 00:34:33.920 |
tkenter and you'll see that all of the examples in it actually use wildcard imports so it's actually 00:34:40.560 |
used in the python standard library itself but it's it's only a good idea it's like it's something 00:34:48.560 |
you'd only want to do for libraries that are explicitly designed to work this way because 00:34:53.040 |
there's a somewhat advanced python feature called dunderall which ensures that things 00:34:57.360 |
work correctly when you do this normally that's a lot of work to create and not really worth the 00:35:02.640 |
effort nb dev libraries do it automatically so one nice thing about nb dev libraries is 00:35:07.840 |
that they work very well in rep or oriented programming because they'll support using 00:35:12.000 |
wildcard imports but again it's something which if you're at an organization that uses kind of 00:35:18.320 |
pet bait and stuff you might want to explicitly import each thing carefully 00:35:23.040 |
um but you know one nice thing about exploratory programming is for people that aren't very 00:35:30.240 |
familiar with their ide they often like don't know where symbols come from or what they mean 00:35:37.440 |
so for example there's a thing in um fast core test core test for quality test act um in jupiter 00:35:45.920 |
you don't have to worry about like you don't have to look up the top to find it because at any point 00:35:49.520 |
you can just stick a question mark after it and it will tell you exactly where it came from 00:35:54.000 |
and furthermore you can actually just put a second question mark and that'll give you the source code 00:35:59.280 |
for it um or you can write it without any question marks and it'll just tell you the details of the 00:36:07.600 |
source and the parameters uh or you can just hit um open parenthesis and then shift tab and you'll 00:36:15.120 |
get all the information here so there's lots of ways you know in a REPL based environment of getting 00:36:20.720 |
all this information without scrolling around and wasting time um so we're going to test that for 00:36:28.800 |
example a card like that should be equal to a card like that um where else a card with a different 00:36:46.560 |
suit should not be equal and a card with a different rank should not be equal 00:36:59.280 |
okay so if we run that it doesn't work um because we haven't defined a quality yet so the way you 00:37:11.840 |
define a quality in python is by defining a dunder equality 00:37:20.240 |
and we could just check that the two tuples are the same so if we check that uh self.suit 00:37:34.240 |
comma self.rank equals oh we don't need b it's going to be past the self yourself and then one 00:37:44.960 |
other thing uh and check that against um a.suit comma a.rank 00:38:01.280 |
there we go so they all pass um now we're not showing uh like totally perfect software engineering 00:38:10.080 |
principles here we should be checking that the types are the same and stuff like that but we 00:38:13.680 |
could at least give some indication um that the types are meant to be a card um this is a slightly 00:38:22.000 |
weird python thing that python doesn't know what a card is into when you're inside a card so nowadays 00:38:27.760 |
you have to put it in quotes um uh you know like i tend to like having my functionality all in one 00:38:40.320 |
place though so what i would tend to do at this point is i would often split this out so in 00:38:45.200 |
jupiter you can hit ctrl shift hyphen to split a cell out and you know i'd quite like to kind of 00:38:52.800 |
have all this stuff defined down here in one place so i'd have like equals not equal you know and so 00:39:01.760 |
forth um so to define a um a method outside of its class which is kind of something that's pretty 00:39:11.120 |
common in things like c++ for example um you can also do it in python using something from 00:39:18.480 |
another thing from fast core so the main things in fast core live inside its uh utils 00:39:26.640 |
and one of the things that will give us is something called patch 00:39:30.640 |
so we can add a quality to fast core just by saying patch and we're just going to say what 00:39:38.080 |
do you want to patch i want to patch card and one nice thing is now that actually exists i 00:39:44.320 |
don't need this weird quote thing anymore so if i now can i define i can define as you can see here 00:39:52.480 |
i've defined card and then i've patched in a quality later and this is part of the what i 00:39:59.760 |
was talking about the dialect of python this is one of the extensions that make it easier to write 00:40:05.360 |
code in a notebook and so i'd be inclined at this point to kind of introduce a section in my 00:40:10.720 |
documentation here um or comparison operators okay um and so we'll also medically we'll 00:40:24.880 |
automatically get documentation for this um so then we could do less than or equal to or less than i 00:40:32.400 |
guess um so we would expect um one three to be less than two three 00:41:29.040 |
so lt and gt is what python uses for less than and greater than 00:41:35.280 |
so this should not be the case for greater than 00:41:52.240 |
and then for less than or greater than we would expect 00:42:10.000 |
all right i think that's basically our playing card um so at this point we can 00:42:21.280 |
um we can try it out and so to create um our card.py file um we can head over to here to our terminal 00:42:34.080 |
and type nv dev underscore export yeah we can type nv dev underscore export 00:42:49.760 |
and you'll see we've now got um an nv dev underscore cards directory 00:43:03.440 |
we've decided not to use core after all so i'll get rid of that 00:43:10.560 |
um so like one thing we could do at this point is to see whether that seems to be working okay 00:43:25.120 |
so you're in the index uh notebooks that's the home page that's going to be our home page exactly 00:43:32.880 |
um so we can say like this web provides a card 00:43:43.920 |
class you can use to create display and compare playing cards 00:43:57.520 |
add one comma three um suits are numbered according to this list 00:44:13.680 |
so you can see all that stuff has been imported into our environment from nv devs.com so that's 00:44:26.080 |
pretty handy so we'll call this nv dev underscore cards 00:44:35.840 |
and we're not automating this bit unfortunately we probably should be we'll have to copy the 00:45:09.840 |
so we might put a link here to where the inspiration for this comes from 00:45:50.000 |
um think python two think python second edition by allen downey 00:46:54.000 |
so the hyphen or underscore yeah so this is a this is our first um little tricky issue 00:47:06.880 |
which is that um hyphen and underscore are a special character in python python modules 00:47:18.320 |
can't have a hyphen hyphen and pi pi i don't think can have an underscore at least you don't 00:47:25.280 |
normally see them so we actually are going to have a different name um so ideally i wouldn't 00:47:31.840 |
have picked a name with an underscore because that's the only basically the only character 00:47:36.320 |
that has this weirdness um but that's okay um so to fix this we have to change it in 00:47:44.560 |
settings.ini to say that we've actually got two different things um so the lib name 00:47:52.640 |
is actually i guess nv dev hyphen cards and then the lib path 00:48:03.840 |
is actually nv dev underscore cards that's a little bit confusing um this uh percent s business is um 00:48:16.240 |
part of the thing from the standard the python standard library called config plaza um it just 00:48:22.640 |
copies the variables from user and lib name up here and you can override them if you like 00:48:31.520 |
um all right um should we maybe have a look at the documentation now 00:48:39.600 |
okay so to preview your documentation um you can type nv dev underscore preview 00:48:51.280 |
and that will fire up a um that will fire up a quarter web server for you um and as you can see 00:49:04.800 |
it will automatically install quarter um if you don't already have it um if you're on linux it'll 00:49:11.600 |
do it automatically i'm on mac though so it's going to pop up this window 00:49:24.080 |
quarter is updated recently regularly so it's not a bad idea to make sure you've got the latest 00:49:31.200 |
version anyway all right so let's run that again now that's installed 00:49:43.280 |
so that's going to build each of my pages and then what's going to happen is it's going to sit 00:49:52.880 |
running in the background and uh and so it's just going to sit there running a little server on port 00:49:57.760 |
3000 so if we now go go to um click on there um it's popped it open here 00:50:08.320 |
and so yeah let's take a look so nv dev cards actually let me pop this on the other 00:50:20.640 |
screens we can compare them more easily so you can see how the heading the summary 00:50:28.560 |
there it all is um if you look at the card page that that's interesting since yeah so let's head 00:50:40.080 |
over here and compare it to our card page all right um okay so you can see all this stuff's been hidden 00:50:56.160 |
and as you can see we've got the auto generated docs here 00:51:08.640 |
okay now this is a mistake this shouldn't be appearing and the reason why is that it's not 00:51:14.080 |
being exported because i didn't export it so i'll copy this export here and i'll paste it here 00:51:21.040 |
there we go and then um i've saved i just hit save and you can see that it's um 00:51:41.680 |
and because these are like start with underscore they're like considered hidden by python so it 00:51:52.080 |
we kind of don't do that because i you know we think a lot of people would not like it's 00:52:02.400 |
it's not that we're creating really something for users called dunder equals so like you could say 00:52:07.440 |
show doc if you felt that your users were quite advanced and would know what that meant 00:52:16.080 |
and it would pop up like so um you know personally i try not to expect my users 00:52:26.240 |
to understand stuff like that so i would rather just kind of put in some markdown i think 00:52:32.240 |
and in fact we could make all this a little bit shorter by then putting all this stuff together 00:53:13.840 |
test of less than and this is this is really cool i mean i find this part to be really nice like 00:53:28.080 |
you can choose which tests you want to show to your users and which ones you don't yeah because 00:53:34.320 |
it makes sense to do it sometimes it doesn't make sense to yeah in fact you know let's just for 00:53:38.960 |
example let's say the you know we just want to show them one test of each 00:53:43.120 |
which is fine like you don't necessarily want to overwhelm them with examples 00:54:12.400 |
yes it's probably not a bad way just one example of most of them okay um so i really like the way 00:54:24.800 |
you get this kind of auto generated documentation i mean now that i kind of know what the docs are 00:54:30.080 |
going to look like i don't use it that much myself unless i'm doing more advanced websites 00:54:36.400 |
an example of a more advanced website would be the nb dev home page 00:54:40.400 |
the nb dev home page is actually generated from a notebook 00:54:48.480 |
and so that one it was certainly helpful to have the auto generated preview 00:54:55.520 |
okay shall we do you want to do you want to show do you want to talk about since we 00:55:05.440 |
spent some time on tests do you want to kind of how do we know do ci now or should we do 00:55:12.160 |
actually do the deck first and then we'll do ci yeah you can but you can show local tests i think 00:55:17.760 |
the low yeah local tests of course i've got about those all right so uh for local tests 00:55:23.200 |
so i'm just going to run this in the background we'll leave that running so if you just type nv dev 00:55:27.680 |
underscore test and that's going to basically make sure all of your tests pass um 00:55:35.360 |
and you know we don't like to give you more information than needed so it just 00:55:38.640 |
tells you if they do let's break one save that 00:55:46.320 |
and so you can see now it tells you that in zero zero card sell 20 00:56:01.440 |
my colors could be better but i'll fix that my term sometime it shows you exactly where 00:56:05.760 |
the problem occurs and it expected three of hearts and it got a three of diamonds and you 00:56:10.800 |
can see it even uses the correct representation thanks to fast core test um and at the end it'll 00:56:18.560 |
summarize a list of any notebooks that failed so we'll go and fix that save it rerun our test 00:56:33.680 |
for these situations to see what you can pass in it'll run the tests in parallel using as many 00:56:43.040 |
workers as you have cpus by default you can see how long they take to run you can look at just 00:56:48.960 |
particular files so forth there's lots of options you can give it i was going to say when you're 00:56:54.080 |
using nv dev and you're debugging in real life um i think it's worth it to mention the hotkey for 00:57:00.080 |
reload and run all um yeah definitely yeah that's that's really useful yeah exactly so you want 00:57:07.440 |
because when you run nv dev test it's going to run your notebook from top to bottom so to rerun your 00:57:12.000 |
notebook from top to bottom if you just hit zero zero that restarts the kernel um and then there 00:57:20.160 |
isn't a hotkey built in for running all cells but it's a good idea to add one by hitting help edit 00:57:26.400 |
keyboard shortcuts let's run them all and we're all good to go 00:57:32.080 |
a lot of i mean a lot of people including me like to explore very interactively in notebooks 00:57:40.480 |
and often go back up and rerun a cell and change things and see what happens um but um yeah that's 00:57:46.240 |
a very good idea from time to time to to hit zero zero and then rerun all the cells or at least to 00:57:52.160 |
head over to your terminal and run nv dev uh test um yeah and another thing is like you know like 00:58:02.000 |
when you get a failed test in your terminal you do what i do um just sharing my workflow is i come 00:58:08.160 |
back and i do restart run all i get the error in the notebook and then i hit the interactive debugger 00:58:12.720 |
like the percent percent debug and i kind of go from there see what's going on yeah so um 00:58:37.360 |
and it will drop you into an interactive debugger which is called pdb it comes with python and you 00:58:45.600 |
can do things like find out the value of any variable like suit you can find out the stack 00:58:52.640 |
trace of where we are you can get a listing of source code and so forth and you can yeah 00:58:58.800 |
basically write any python expression you like um figure out how to fix the bug 00:59:10.320 |
and away you go all right let's do a deck of cards 00:59:17.120 |
so we'll export this to something called deck 00:59:33.120 |
so generally speaking you know in your second 00:59:52.240 |
notebook it's pretty likely you're going to want to import this stuff from your previous notebook 01:00:00.960 |
and at the moment i won't be able to do that actually yes hang on can i let's try it 01:00:07.360 |
maybe because you did an editable install oh uh cards i think got it wrong yeah the card yeah 01:00:17.440 |
did i do a local install i don't remember doing that i guess i must have 01:00:25.120 |
so the reason that works i guess or maybe i did that without even thinking about it if you type 01:00:29.920 |
pip install minus e dot in your git repo basically it's going to install the thing you're currently 01:00:37.920 |
working on as a library and it's going to be pointing at your actual source code so every 01:00:42.800 |
time you update it it'll be there so that's how come i can import it um all right so and now you 01:00:52.400 |
might want to put the import in a separate cell and export it perhaps yeah exactly so let's do 01:00:59.680 |
that because that's actually going to be part of the library is that we're probably going to want 01:01:04.000 |
to use these cards so that should be part of the exported and actually we should probably look at 01:01:11.600 |
that so let's take a look at um oops i just managed to break everything did i oh um 01:01:19.760 |
let's just make sure this is actually going to run 01:01:26.080 |
there we go so because i've got the um um quarter server nb dev server running in the background 01:01:33.520 |
it's constantly trying to compile my code so i had it in a non-compiling state just before 01:01:37.680 |
just write complain um so if i take a look at the nb dev cards card file you can see this is the 01:01:47.760 |
source that's created for me um and so for example if we now like let's see 01:02:13.600 |
um every for every suit and for each suit for every card it's going to contain a card 01:02:31.200 |
so we can do the same trick we've seen before which is to 01:02:41.760 |
just join the cards together when we're stringifying it and set the representation 01:02:55.440 |
so now if we might just stop running our server for a bit a bit annoying um so when we could now 01:03:13.360 |
we've now got a deck.py and you know you can um 01:03:24.000 |
treat this just like you would any normal source code so for example i use vim so if i go to card 01:03:31.600 |
and i hit ctrl right square bracket and vim it jumps me straight to the definition of card 01:03:36.160 |
um so like you can do what you know i can jump back so you can still like you know if you use vs 01:03:43.840 |
code you can still use it just like the normal way that you would look through source code 01:03:49.200 |
or of course we can use the trick we've seen before for example double question mark to get 01:04:07.360 |
oh yeah because it's not exported your fast core utils test oh yeah that was a mistake 01:04:28.080 |
okay so that needs to be exported because that's part of what we actually need 01:04:34.640 |
you should put all your imports in the separate cell by itself maybe it's a good time to 01:04:43.440 |
mention that oh and then just a moment so then we need to export the hat 01:04:48.240 |
and so then we could check that that's working 01:05:05.040 |
oh sorry i shouldn't look at deck i should look at card example there you go you can get the 01:05:20.560 |
source code all right so um so Hamilton started talking about a um one little wrinkle you have 01:05:31.360 |
to be aware of when creating your code in nvdev which is that there's one um golden rule you have 01:05:40.720 |
to keep in mind and that golden rule is that you your cells should either contain imports 01:05:49.840 |
or non-imports but not a mixture of the two so you'll see this doesn't contain any imports 01:06:00.240 |
this doesn't contain anything but imports um and the reason for that is that when it builds your 01:06:05.680 |
docs it has to be able to go through and run every one of your cells containing imports 01:06:12.480 |
in order that it can then run all of your show doc cells correctly but it's not going to run 01:06:17.440 |
any other cells so just remember don't have a cell that contains both a import command and also 01:06:25.840 |
something else that's the only slightly weird rule you have to remember 01:06:36.960 |
i think something else that i'd like to add to our deck is just to know how big it is 01:06:52.960 |
and so by so let's first create a deck call it deck 01:06:57.440 |
i notice a difference in case here this is my object and this is my class and this is my 01:07:11.200 |
instantiated object there we go another thing that's useful is to know if a card is in the deck 01:07:27.360 |
so in python they use a special dunder thing called dunder contains for that 01:07:36.320 |
let's see if the um so just remind myself what are the 01:08:29.600 |
all right i guess um we might want to create a hand now but you might want to then deal a hand 01:08:36.480 |
or something right um well yeah but probably to do that we want to be able to um select a card 01:08:48.480 |
from the deck so i guess first of all let's just uh see if we've got all the information we need 01:08:59.200 |
here so um let's say when we initially create a deck four of the cards will be present 01:09:18.720 |
that should be 52 cards and so this is where i'd put a test rather than just displaying it 01:09:30.720 |
because that way we're both showing the user what we're expecting and we're also ensuring 01:09:37.040 |
that that continues to be the case in the future just as a reminder for people not familiar the 01:09:44.240 |
test eq is a wrapper around a cert that just will give you a nicer error message yeah pretty much 01:09:51.520 |
the cert that they're equal so if they're not it'll let you know what they actually were 01:09:58.240 |
so as you can see like most of the fast ai library code tends to be just a couple of lines so this 01:10:05.680 |
line this is one line of code this function's defined with two lines of code um so normally 01:10:12.240 |
you can like yeah we've i mean there's good documentation for all this stuff of course 01:10:16.000 |
so if you go to the fast core docs you can go to test and see examples of all of them 01:10:24.960 |
yeah but often you can just quickly look at the source code if you want to see exactly what's 01:11:12.720 |
okay so let's make it so that we can remove something from the deck so we could just go 01:11:16.720 |
ahead and edit the the class but as i say i kind of like to add things in after the fact you know 01:11:24.000 |
just keep things nice and separated uh we should generally it doesn't matter whether you have a 01:11:30.640 |
space or not um after your pipe character but i know that at least at the moment in 01:11:39.040 |
knitter which is an r library it doesn't like the space so it's probably not a bad idea 01:11:46.960 |
most of the stuff you'll see in all the stuff you'll see in quarter always has a space 01:13:28.240 |
um so let's try popping something so if we pop something we should get the king of 01:14:26.640 |
okay so let's export that make sure everything's running okay it is 01:14:55.600 |
so we can now say we've got our index page our card page and our deck page are all here 01:15:19.600 |
so one thing you'll notice here is that the back tick we used here for card 01:15:35.440 |
to the documentation for that um i won't be able to click on it right now because it's um 01:15:43.520 |
okay because we haven't put it on github yet um but this will end this will link to 01:15:48.320 |
in effect i just copy the link you'll get the idea copy link address paste it so that's where 01:15:54.800 |
our documentation for that's going to end up being so you can just use back ticks and it will 01:15:59.760 |
automatically linkify as we call it um that to the documentation 01:16:11.120 |
and it works across all your bdev projects uh yeah because exactly we have yeah and not just 01:16:21.200 |
mvdev projects in fact there's um there's a number of other things including the python standard 01:16:28.560 |
library and pandas and numpy and so forth that you can install indexes for those so that they'll 01:16:36.080 |
automatically linkify as well um cool so then you know we can show that this seems to be working 01:16:43.520 |
correctly um that's going to rerun this okay so um so just rerun that from the top to make 01:16:55.920 |
that all work again um all right so that should be um 01:17:05.200 |
in our deck now you can see that that's auto generated that documentation 01:17:23.600 |
and we can also like check that pops behaving the way we'd expect it to be 01:17:39.760 |
so i'm just going to copy and paste this one from when i built before to save a bit of time for us 01:17:52.080 |
and so again we can check to see how the preview is looking 01:18:10.080 |
and so when we see this is not appearing here we know what we forgot to export it 01:18:42.240 |
so it's not just for methods we could also create a function so i've got a function that i've created 01:18:58.240 |
so here's one that draws some cards with all our replacement 01:19:37.040 |
if we just put that straight into the class just to show how that would look there's a shuffle 01:19:43.120 |
so since the shuffle here is directly in the class we need to say whereabouts we want to 01:19:50.160 |
document it so the way we would do that would be to put a show doc 01:20:29.360 |
oh one thing i should mention it's okay to put an import statement in an exported cell 01:20:34.560 |
because they're going to be run either way so that's the one difference is you can put 01:20:38.160 |
an import inside a cell that's exported there we go 01:20:43.520 |
let's take a look at how our documentation is looking 01:20:54.640 |
nice all right ammo where should we head now um oh um 01:21:03.840 |
did we want to create the command line interface 01:21:08.240 |
yeah sure are you want to do a visualization to make sure your 01:21:15.280 |
draws are doing or okay um i mean that's already the tutorial it's not really anything 01:21:21.040 |
extra okay no problem yeah i mean the ci lies in the actually the ci lies in the written 01:21:25.520 |
new tutorial as well it's also really not anything extra so because this is already pretty long um 01:21:30.880 |
and it's not really mv dev specific so yeah check out the um the the written version of 01:21:37.520 |
the tutorial of this online for a couple more um handy little tricks um 01:21:56.960 |
to send it up to github one thing that i like to do is to make sure that there's like no unnecessary 01:22:07.360 |
metadata in the notebooks and so um you can manually check do that by using nb dev clean 01:22:18.000 |
there's also some githubs you can install to do this automatically you can look up in the docs 01:22:21.760 |
if you're interested um and so if you um have a look now we can see what's what we've got 01:22:33.680 |
ready to go to github um so we can add all that um yeah not a bad idea just to 01:22:53.280 |
so they're basically the three things uh check the files so there's going to be 01:23:00.720 |
continuous integration added doc documentation deployment added uh the notebooks we created 01:23:10.560 |
some information about the website our home page um the two modules we created 01:23:18.080 |
our settings file setup file sidebar and a style sheet that all sounds good to me 01:23:35.600 |
okay so that gets sent off to github where we should find it 01:23:42.480 |
there it is and so github has something called github actions that automatically 01:23:53.840 |
runs things that are in this github github workflows folder um so one thing you'll find here 01:24:02.560 |
is that when you push or when somebody puts in a pull request um it's going to 01:24:09.520 |
run nb dev test amongst other things and it will also 01:24:18.320 |
uh run something called quarter ghp which will set up your github pages so while we wait for that 01:24:29.520 |
i'm going to go into pages and tell it that gh pages is the branch so that's 01:24:45.360 |
okay and so if we look at actions we can see there we are it's that's all run 01:24:55.120 |
so the ci sensor continuous integrations that means our tests are passed and the deployment 01:25:01.040 |
was completed so you can actually click on these things to see them or the steps that they run 01:25:07.600 |
and you'll see that installs all the python stuff and you can see it 01:25:13.680 |
basically looks the same as our quarter did when we ran stuff locally 01:25:19.680 |
and then finally automatically github creates our web page for us 01:25:25.280 |
and so you can see it's visually building it there looks like it's finished building it 01:25:30.720 |
okay it's finished and you can see it tells us here where it's been built too 01:25:54.000 |
there's our website awesome now links are working 01:26:14.080 |
okay so that's looking good um shall we put this on pai pai hamel yeah 01:26:28.400 |
i just want to point out like most projects at this point you would only just have code 01:26:35.440 |
so at this point we have ci we have a documentation site and this is the point 01:26:43.600 |
where you know when i'm personally coding and i'm kind of down at this point and it's exciting 01:26:48.720 |
because i just like my colleague you know i just say hey like i created that tool you wanted 01:26:53.680 |
and then here's the website and they're like you have a website i'm like yeah check it out 01:27:00.480 |
you can install it yeah yeah and then they're like wow it's and then also you can just pip 01:27:06.160 |
install it and also you have ci like how did you do all this such a short amount of time 01:27:20.720 |
our readme is not working oh you know the other thing we should have done is run nb dev docs 01:27:36.240 |
and um the reason for that is that um so that's kind of does a trial run of the same thing that 01:27:46.880 |
um that it's going to have that github actions is going to do um but what it also does 01:27:54.720 |
is it updates our readme to contain the same contents as the homepage does 01:28:02.800 |
um keep forgetting i haven't got this alias installed 01:28:10.240 |
so if we now look the other thing which we've got is 01:28:26.160 |
a beautiful readme uh so that comes from our index.ipy and b and it's going to be the same as 01:28:33.600 |
the home page it won't be as beautiful as the home page because it's using more limited markdown but 01:28:37.840 |
the basic you know the basics are there um okay so we can see that there's an nb dev pipy to upload to 01:28:51.680 |
pipy and there's a another one for conda doing conda or just nb dev release to do both 01:28:58.960 |
i'm just going to do pipy for now all right so let's run it 01:29:09.680 |
and just at the end it's also automatically bumped the version 01:29:22.400 |
so i tend to like just go git commit -am bump 01:29:48.320 |
there it is and you can see it's even put the project description project home page 01:30:01.840 |
it's all been done automatically which i think is all the metadata it looks like a 01:30:10.800 |
very professional and polished library for something that we spent under two hours on 01:30:20.800 |
you know it's not just for little quick two-hour things like this you know i've 01:30:27.600 |
well you and i have both written libraries that have taken years 01:30:31.440 |
with thousands of lines of code and dozens of modules and tens or hundreds of thousands of users 01:30:36.800 |
yeah no it's great and imagine what it's like to make a pr into one of these projects so 01:30:44.480 |
let's just say i don't have any idea how deck works at all i'm like what is this deck thing 01:30:50.320 |
i've never seen it i don't know and um without knowing anything about the code i just go to that 01:30:57.520 |
notebook and there's all there's already the entry point for me there's the code and then there's 01:31:02.080 |
sample based on how to run it with documentation right there so there's no confusion and then i can 01:31:09.760 |
you know i've noticed that we got a lot of pull requests with just high quality ones making the 01:31:14.560 |
documentation better making the tests better because people read it they get confused and 01:31:19.920 |
they just resolve their confusion they just say i'm just going to edit this real quick and submit 01:31:24.160 |
a pr yeah i mean let's let's have a look at an example so yeah fastai is an example of a package 01:31:29.680 |
which uses mbdev it's got over 20 000 stars on github over 7 000 forks so here's an example of 01:31:39.440 |
an open pr and one thing that we'd use which is fine is there's a thing called review nb 01:31:44.640 |
it actually means i can click a button here and because it's yeah because it's all in notebooks 01:31:50.160 |
you know i can immediately see the documentation that i've added and i can make comments on the 01:31:56.480 |
documentation and if you know i can see the source code that's been changed i can see if 01:32:02.240 |
they've added or removed any tests i can see if any of the output graphs or whatever have changed 01:32:08.560 |
and yeah it's it's it's nice both for me as the person reviewing that pr and also as you say for 01:32:19.200 |
the person making that pr that we're very much all on the same page about what's about what's 01:32:25.200 |
going on so yeah i find we don't get pretty pretty high quality pull requests to our projects 01:32:30.320 |
and what's great is so this is only kind of the tip of the iceberg like there's a lot of exciting 01:32:38.160 |
things so you can use the same tools that we showed today to just make a website let's say 01:32:44.640 |
you don't want to necessarily write code maybe you want to just write a blog so we'll have 01:32:50.960 |
a tutorial or video like at some time in the near future that shows okay how do you do write a blog 01:32:56.720 |
and then also you can just have a website too that maybe you just want to make a tutorial on 01:33:02.000 |
let's say how to use fastai or how to use your favorite library you can use same set of tools to 01:33:06.480 |
do that and there's all kinds of advanced stuff that you could do too like quartile is very 01:33:11.920 |
powerful you can make books from it slides all kinds of different formats and so it's the same 01:33:19.200 |
you know for making any kind of technical content uh as well so yeah it's it's really exciting yeah 01:33:25.440 |
and it's a whole different philosophy of how to write software um and uh yeah i think it's 01:33:31.760 |
you and i've both got the experience now that it's made us more productive and we're having 01:33:36.080 |
more fun so it's good to be doing this tutorial to think about people watching it that are going to 01:33:40.880 |
join this journey as well so thanks everybody for watching and hamil thanks so much for joining in 01:33:47.120 |
the tutorial um late at night before you're doing a keynote tomorrow i really appreciate it all right