Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
duck monster
Dec 15, 2004

Zugzwang posted:

Yeah I really wish Python had something like "type hinting is purely optional, but they will be enforced if you specify them." As opposed to "the Python interpreter does not give the slightest gently caress about type hints."

Julia has the former as part of its multiple dispatch model, but sadly, adoption of Julia continues to be pretty underwhelming.

loving VB6 had a solution for this.

"Option Explicit On"
"Option Strict On"

It would be a highly good thing to have the option to just stick something like that at the top of a python file, so that it explodes at import if it fails a sanity linting.

Also re "poisonwomb" mentioned earlier, I assume thats a reference to "your poison womb is making heaven crowded". Because good god does this website have some shady loving history at points (For those blessed not to remember that, its one of our more shameful moments. Keep in mind, at that point, the usual result of getting your terrible website linked off here was pretty much identical to what happens if 4chan does it now. But in this case it was women who where having somewhat traumatized responses to stillbirth. Yeah, not so funny now we're all 40somethings....)

Adbot
ADBOT LOVES YOU

Gothmog1065
May 14, 2009
Entire script: https://github.com/jmruss10/netParser/blob/main/netParser.py

current error: https://github.com/jmruss10/netParser/blob/main/errors.txt

So I got to playing more yesterday, it seems that setting args as global changes it globally (at least below/after main). Now I'm getting a new error, but this is probably more me not understanding the argparse.

Macichne Leainig
Jul 26, 2012

by VG

duck monster posted:

loving VB6 had a solution for this.

"Option Explicit On"
"Option Strict On"

It would be a highly good thing to have the option to just stick something like that at the top of a python file, so that it explodes at import if it fails a sanity linting.

That's basically also how flow and whatnot worked in Javascript for the longest time (and may still work that way, but IDK why you would use Flow now when Typescript exists)

QuarkJets
Sep 8, 2008

Gothmog1065 posted:

Entire script: https://github.com/jmruss10/netParser/blob/main/netParser.py

current error: https://github.com/jmruss10/netParser/blob/main/errors.txt

So I got to playing more yesterday, it seems that setting args as global changes it globally (at least below/after main). Now I'm getting a new error, but this is probably more me not understanding the argparse.

The error is saying that args is not defined at this line:
https://github.com/jmruss10/netParser/blob/main/netParser.py#L113
Python code:
def data_display(dataDict):

    thdList = []
    for key in dataDict.keys():
       if re.search(r'^protocol', key):
            # ...
    thdList=(sorted(thdList, key = lambda x: x[args.sort]))  # <------------- this line
    for line in thdList: print(line[0],line[1], line[2], line[3])
And that's true! You have an args variable in main(), but not in data_display(). In main() you could pass args.sort to data_display() as an argument,

Python code:
def def data_display(dataDict, do_column_sort=True):
    # ...
    if do_column_sort:
        thdList = list(sorted(thdList))
    for line in thdList:
        print(line[0],line[1], line[2], line[3])

def main():
    # ...
    do_column_sort = bool(args.sort)
    data_display(dataDict, do_column_sort)
You should try to avoid using variables in the global scope, your code will be cleaner and easier to read. Use of the "global" keyword is a code smell, you're usually better off without it. In this code the dataDict variable is sometimes handled as a global variable and sometimes handled as a local variable passed in to a function call, and that's confusing to deal with - why not always make it an argument in function calls instead?

Looking at all of the uses of dataDict as an example:
- add_to_dict() should accept the dictionary as an input
- create_dict() doesn't really need to accept any inputs, it can just create an empty dictionary and return it instead of modifying the globally-scoped one.
Python code:
    dataDict = create_dict()
    data_display(dataDict, do_column_sort)

Gothmog1065
May 14, 2009

QuarkJets posted:

The error is saying that args is not defined at this line:
https://github.com/jmruss10/netParser/blob/main/netParser.py#L113
Python code:
def data_display(dataDict):

    thdList = []
    for key in dataDict.keys():
       if re.search(r'^protocol', key):
            # ...
    thdList=(sorted(thdList, key = lambda x: x[args.sort]))  # <------------- this line
    for line in thdList: print(line[0],line[1], line[2], line[3])
And that's true! You have an args variable in main(), but not in data_display(). In main() you could pass args.sort to data_display() as an argument,

Python code:
def def data_display(dataDict, do_column_sort=True):
    # ...
    if do_column_sort:
        thdList = list(sorted(thdList))
    for line in thdList:
        print(line[0],line[1], line[2], line[3])

def main():
    # ...
    do_column_sort = bool(args.sort)
    data_display(dataDict, do_column_sort)
You should try to avoid using variables in the global scope, your code will be cleaner and easier to read. Use of the "global" keyword is a code smell, you're usually better off without it. In this code the dataDict variable is sometimes handled as a global variable and sometimes handled as a local variable passed in to a function call, and that's confusing to deal with - why not always make it an argument in function calls instead?

Looking at all of the uses of dataDict as an example:
- add_to_dict() should accept the dictionary as an input
- create_dict() doesn't really need to accept any inputs, it can just create an empty dictionary and return it instead of modifying the globally-scoped one.
Python code:
    dataDict = create_dict()
    data_display(dataDict, do_column_sort)

Thank you for the input. My only frustration I guess is having to pass the args variable around since I'm going to be needing it as I expand the scope of the script and add more arguments. However, the dictionary can definitely be de globalized. I know there's going to be a lot of cleanup to be done as I work through this and add more functionality to it.

Most of my variable scoping came from bash/kornshell so it's going to take a bit to get rid of some of those habits.

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug

Gothmog1065 posted:

Thank you for the input. My only frustration I guess is having to pass the args variable around since I'm going to be needing it as I expand the scope of the script and add more arguments. However, the dictionary can definitely be de globalized. I know there's going to be a lot of cleanup to be done as I work through this and add more functionality to it.

Most of my variable scoping came from bash/kornshell so it's going to take a bit to get rid of some of those habits.

FWIW scope is going to be a big part of dev so it's worth thinking about it. It's very easy at first to take an approach of 'I'll throw everything around everywhere' but it's probably worth considering that it makes testing and reusing code a lot more complicated, and unit tests are very helpful as your program gets larger.

For example, I wouldn't pass around your args object, I would extract everything you need out of it into independent variables, and then pass those individual elements into functions where required - you might have a function that needs to know what sort option you're using, but might not care about filename/etc.

Edit: One big reason to do the above is that the args object produced by argparser doesn't have any type hinting/etc, so once you pass that down a few layers you have to constantly go back to your argparser to be like 'wait what is the sort attribute called? And what type is it?'. Extracting sort into a variable and then passing it explicitly (with type hinting) makes this a ton easier on you in the future as your IDE will automatically know what type it is (as well as the variable.)

Falcon2001 fucked around with this message at 03:47 on Sep 15, 2023

QuarkJets
Sep 8, 2008

Gothmog1065 posted:

Thank you for the input. My only frustration I guess is having to pass the args variable around since I'm going to be needing it as I expand the scope of the script and add more arguments. However, the dictionary can definitely be de globalized. I know there's going to be a lot of cleanup to be done as I work through this and add more functionality to it.

Most of my variable scoping came from bash/kornshell so it's going to take a bit to get rid of some of those habits.

I want to discourage you from using globalization, it's not the best way to handle CLI arguments. You should have a function whose sole purpose is to parse an object from argparse into whatever information your code needs. Defining args as a global object means defining argument-parsing behavior in multiple places, and that can lead to trouble later.

If you define your own dataclass that holds the parameters that can be set via the CLI, then you can have a factory function or class method that returns an instance of that dataclass after parsing the results from argparse. If you wind up have conflicting flags then the logic for handling that gets to exist in just 1 place, and that way you can also keep adding as much CLI scope as you want while knowing that all of your CLI parsing is in a single place

Gothmog1065
May 14, 2009
So wrapping my smoothbrain around this, hopefully I'm getting what you're saying. I'd have something more akin to the following (I'm typing this on the fly, just trying to get a more generalized sense of what I'm doing):

Python code:
from dataclasses import dataclass
import re, argparse

@dataclass
class Parser: 
    filename: str = "filename.txt"
    shortPath: str = "/home/user/Desktop/"
    site: str
    sort: int

    parser = argparse.ArgumentParser(description = "NetConfig Display module")
    parser.add_argument("site", help="Site to load from", required=True)
    parser.add_argument("-s", "--sort", help="Sort by column.", default=1, required=False)
    parser.parse_args()
    args = parser.parse_args()

    if <site checks and whatnot>: 
        site = args.site
        fullPath = shortPath + site + "/"

    sort = args.sort

def create_dict():
    file = Parser.path + Parser.file

    #...

    return dataDict

def data_display(dataDict):
    # ...

    thdList=(sorted(thdList, key = lambda x: x[Parser.sort]))
    for line in thdList: print(line[0],line[1], line[2], line[3])

def main():

    parse = Parser()
    dataDict=create_dict
    data_display(dataDict)
Is that more in line with what I need, or did I just drive myself off a cliff? Also, if I'm not mistaken, that is immutable, so I can't change those attributes after I initialize it.

QuarkJets
Sep 8, 2008

Gothmog1065 posted:

So wrapping my smoothbrain around this, hopefully I'm getting what you're saying. I'd have something more akin to the following (I'm typing this on the fly, just trying to get a more generalized sense of what I'm doing):

Python code:
from dataclasses import dataclass
import re, argparse

@dataclass
class Parser: 
    filename: str = "filename.txt"
    shortPath: str = "/home/user/Desktop/"
    site: str
    sort: int

    parser = argparse.ArgumentParser(description = "NetConfig Display module")
    parser.add_argument("site", help="Site to load from", required=True)
    parser.add_argument("-s", "--sort", help="Sort by column.", default=1, required=False)
    parser.parse_args()
    args = parser.parse_args()

    if <site checks and whatnot>: 
        site = args.site
        fullPath = shortPath + site + "/"

    sort = args.sort

def create_dict():
    file = Parser.path + Parser.file

    #...

    return dataDict

def data_display(dataDict):
    # ...

    thdList=(sorted(thdList, key = lambda x: x[Parser.sort]))
    for line in thdList: print(line[0],line[1], line[2], line[3])

def main():

    parse = Parser()
    dataDict=create_dict
    data_display(dataDict)
Is that more in line with what I need, or did I just drive myself off a cliff? Also, if I'm not mistaken, that is immutable, so I can't change those attributes after I initialize it.

You'd want to create a factory function that instantiates the dataclass. So three things are defined

1) The dataclass, which just holds the parameters that are relevant to your software
2) main() function, which creates an args object with argparse
3) A function that connects 1 and 2: given args, create an instance of the dataclass and return it

Python code:
from dataclasses import dataclass
import re, argparse

@dataclass
class Parameters: 
    filename: str = "filename.txt"
    fullPath: str
    shortPath: str
    site: str
    sort: int

def parameters_from_args(args):
    parameters = Parameters() 

    if <site checks and whatnot>: 
        parameters.site = args.site
        parameters.fullPath = "whatever" 

    parameters.sort = args.sort
    return parameters

def main():
    parser = argparse.ArgumentParser(description = "NetConfig Display module")
    parser.add_argument("site", help="Site to load from", required=True)
    parser.add_argument("-s", "--sort", help="Sort by column.", default=1, required=False)

    args = parser.parse_args()
    parameters = parameters_from_args(args)
    # now pass parameters to the real entrypoint of your code

Dataclasses are not immutable unless you set frozen=True in the decorator

WHERE MY HAT IS AT
Jan 7, 2011
Edit: ^^^ :argh:

Dataclasses aren't immutable unless you do @dataclass(frozen=True).

You're still relying on this global variable in create_dict which you don't want, and your Parser (I'd probably call it COnfig or something to be more clear that it doesn't do any parsing itself) is tightly coupled to CLI args. Maybe today you only take CLI args to configure this, but someday it may come from env vars, or a config file, or a network call, etc. What you're aiming for is to separate "the config" and "where the config comes from" in most of the code, so it doesn't have to care.

Python code:
@dataclass(frozen=True)
class Config:
    filename: str
    short_path: str
    site: str
     sort: int

    def full_path(self) -> str:
        return self.shortPath + self.site + "/"

def parse_config_from_cli_args() -> Config:
    parser = argparse.ArgumentParser(...)
    ...
    args = parser.parse_args()
    # whatever validation of those args that needs to happen
    
     return Config(filename = args.filename, short_path = args.short_path, ...)

def create_dict(config: Config) -> dict:
    file = config.full_path
    ....
    return data_dict

def data_display(data: dict, config: Config):
         thd_list=(sorted(thd_list, key = lambda x: x[config.sort]))
         for line in thd_list: print(line[0],line[1], line[2], line[3])

def main():
    config = parse_config_from_cli_args()
    data_dict = create_dict(config)
    data_display(data_dict, config)
Now all your CLI arg validation is in one place, if you want to swap the config source somewhere later down the road you can without disturbing create_dict or data_display, and since they both take everything they need as arguments it's easy to write tests for them without needing to set up a bunch of global state.

Personally, I would avoid passing the entire config object to data_display since all it needs out of it is the sort config. Passing around a big bundle of state like that can make it hard to reason about what exactly a function relies on and has led me to some hair-pulling debugging sessions in the past. Or put another way, if a year from now you need to make some changes or fixes to data_display (or call it from some other part of the codebase), which of these function signatures tells you the most about how it works?

Python code:
# all globals!
def data_display():
    ...

# Global config
def data_display(data_dict: dict):
    ...

# Big bundle o' state
def data_display(data_dict: dict, config: Config):
    ...

# Pull out the sort
def data_display(data_dict: dict, sort: int):
    ...

WHERE MY HAT IS AT fucked around with this message at 16:45 on Sep 15, 2023

oatmealraisin
Feb 26, 2023
This might be a dumb question, but what are the benefits of a data class over a regular class?

Chin Strap
Nov 24, 2002

I failed my TFLC Toxx, but I no longer need a double chin strap :buddy:
Pillbug

oatmealraisin posted:

This might be a dumb question, but what are the benefits of a data class over a regular class?

Free string representation, free to_dict implementation, lots of that sort of thing.

If what I'm doing at all looks like "maybe just holding data" I'll use it

QuarkJets
Sep 8, 2008

oatmealraisin posted:

This might be a dumb question, but what are the benefits of a data class over a regular class?

Convenience. If your constructor is just creating a bunch of attributes, then using the dataclass decorator lets you avoid writing a __init__ method and you'll only have to write each attribute name once instead of two or three times. With the frozen option you can emulate immutability without having to make up your own mechanism for that, which can be handy! And it gives you the other nice things that Chin Strap mentioned; a nice __repr__ method is very good and I often neglect to write one, so it's great when I can get one for free in a way that also lets me write less code.

oatmealraisin
Feb 26, 2023
Those free methods and lack of redundant init are enticing, thanks!

Gothmog1065
May 14, 2009
So frustrated now, but lead tech doesn't want to "introduce another language", so going to have to rewrite this in kornshell (because heaven forbid we use something slightly more updated), if not, gonna have to write it in C or C++ most likely. so yeah..


:suicide:

Zugzwang
Jan 2, 2005

You have a kind of sick desperation in your laugh.


Ramrod XTreme
Why write something in Python if C++ will do the job??

oatmealraisin
Feb 26, 2023

Gothmog1065 posted:

So frustrated now, but lead tech doesn't want to "introduce another language", so going to have to rewrite this in kornshell (because heaven forbid we use something slightly more updated), if not, gonna have to write it in C or C++ most likely. so yeah..


:suicide:

Wtf kornshell? That's a terrible decision, extra few months of training time for every new hire.. also kornshell

Presto
Nov 22, 2002

Keep calm and Harry on.
I think this is the first time I've seen the word "kornshell" in maybe 15 or 20 years.

DoctorTristan
Mar 11, 2006

I would look up into your lifeless eyes and wave, like this. Can you and your associates arrange that for me, Mr. Morden?
I’ve always wondered what goes through the mind of someone who decides to build their operations around some obscure fork that hasn’t seen a release in a decade

WHERE MY HAT IS AT
Jan 7, 2011
I can only assume they're just too close to retirement to want to learn something new, even if it's demonstrably better. Not that people earlier in their careers can't be stubborn or cling to garbage, either. Just IME that's been where I've seen it.

Twerk from Home
Jan 17, 2009

This avatar brought to you by the 'save our dead gay forums' foundation.

Gothmog1065 posted:

So frustrated now, but lead tech doesn't want to "introduce another language", so going to have to rewrite this in kornshell (because heaven forbid we use something slightly more updated), if not, gonna have to write it in C or C++ most likely. so yeah..


:suicide:

I used to think that I Got the Life with kornshell, but then after I Did My Time I felt like it was all Coming Undone.

Gothmog1065
May 14, 2009

WHERE MY HAT IS AT posted:

I can only assume they're just too close to retirement to want to learn something new, even if it's demonstrably better. Not that people earlier in their careers can't be stubborn or cling to garbage, either. Just IME that's been where I've seen it.

Pretty much both of these. The guy is smart, but really stuck in his ways, and most people pretty much just go "Well, this is his way so might as well deal with it". He's been here for 25 years or something so he also has a lot of pull, and not worth fighting over it. Just do it like he wants (if I can) and move on.

rowkey bilbao
Jul 24, 2023
How should I write this to convince flake8 to calm down:

Python code:

from fastapi import APIRouter
from typing import Union, Any
router = APIRouter()

USERS_AND_ANIMALS = [... for _ in something]  # dynamicaly loading some pydantic models

@router.post("/", status_code=201, response_model=Union[*USERS_AND_ANIMALS])
def create_user_or_animal(data: Any):
    pass

That snippet behaves like I want to, but our CI spits a warning at the @router line.
The warning is:

code:

.../file.py:9:52: E999 SyntaxError: invalid syntax. Perhaps you forgot a comma?

rowkey bilbao fucked around with this message at 10:05 on Sep 21, 2023

Chin Strap
Nov 24, 2002

I failed my TFLC Toxx, but I no longer need a double chin strap :buddy:
Pillbug
https://stackoverflow.com/questions/62919271/how-do-i-define-a-typing-union-dynamically

Use a tuple instead of a list?

rowkey bilbao
Jul 24, 2023
Thank you Chin Strap

Additionally, where can I read about types in a way that helps me understand wtf it means when I see TypeVar or T or funny brackets ?

This is a huge blind side of mine and this is going to bite me in the rear end some day.

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug

rowkey bilbao posted:

Thank you Chin Strap

Additionally, where can I read about types in a way that helps me understand wtf it means when I see TypeVar or T or funny brackets ?

This is a huge blind side of mine and this is going to bite me in the rear end some day.

https://peps.python.org/pep-0484/ it's a little dry, but the PEP is a pretty good place to start if you're talking about Python type hints specifically. If you're just talking about the notation style, I honestly dunno.

SurgicalOntologist
Jun 17, 2004

rowkey bilbao posted:

How should I write this to convince flake8 to calm down:

Python code:
from fastapi import APIRouter
from typing import Union, Any
router = APIRouter()

USERS_AND_ANIMALS = [... for _ in something]  # dynamicaly loading some pydantic models

@router.post("/", status_code=201, response_model=Union[*USERS_AND_ANIMALS])
def create_user_or_animal(data: Any):
    pass
That snippet behaves like I want to, but our CI spits a warning at the @router line.
The warning is:

code:
.../file.py:9:52: E999 SyntaxError: invalid syntax. Perhaps you forgot a comma?


This is a bit of a smell IMO. Much better would be to setup the classes in `USERS_AND_ANIMALS` such that they have the same parent class. If not possible through inheritance, you can use a Protocol, which is essentially a way to define a class based on its behavior (e.g. any class with methods .feed() and .eat() is an Animal) without needing to touch the class implementations. I'm not super knowledgeable on pydantic if there's any trick there but probably a superclass is the easiest solution here if you have control over those implementations.

Edit: I think Unions and the word "or" in names is a smell to be clear. Certainly a big Union like this one, and there's a better way to type it for sure. But I would also add that create_user_or_animal is also a bit smelly. I don't know your domain and maybe there's a good reason, but I think most likely your code would be more maintainable with different functions/endpoints for creating animals and creating users.


rowkey bilbao posted:

Additionally, where can I read about types in a way that helps me understand wtf it means when I see TypeVar or T or funny brackets ?

Those are generics, mypy docs are pretty good on that: https://mypy.readthedocs.io/en/stable/generics.html

Actually mypy docs are good in general, the section on Protocol is probably better than the one I linked above.

SurgicalOntologist fucked around with this message at 18:51 on Sep 24, 2023

huhu
Feb 24, 2006
I have two classes Plotter and Layer. A Plotter consists of an array of layers. Each layer is an array of instructions.

code:
class Plotter:
    def add_layer(self, name: str):
        self._layers[name] = Layer(self)

    def update_layer(self, name: str) -> Layer:
        return self._layers[name]
code:
class Layer:
    def add_line(self, x1, y1, x2, y2):
        self.add_pen_down_point(x1, y1)
        self.add_point(x2, y2)
I've removed a bunch of code for brevity.

If I want to update a layer, I currently do the following:

code:
plotter.update_layer(RED_LAYER).add_line(0, 0, 50, -50)
This feels bad, is there a more pythonic way?



And one more question. I've got a directory structure

code:
project
    main.py
    __init__.py
demos
    demo1.py
    __init__.py
I want to import project into demos. However, when I cd into the demos directory and try and run any variation of import statements, I get various errors. I've Googled a bunch and I cannot figure out what's wrong with my import. Or is there a better way to make a demos directory for a package I'm building? One of the stack overflow answers says
code:
If the directories aren't Python packages, you can do this by messing around with sys.path, but you shouldn't.

So I feel like I'm missing something here.

huhu fucked around with this message at 18:53 on Sep 25, 2023

Macichne Leainig
Jul 26, 2012

by VG
What is the benefit of adding those functions versus making the class' layers property public and just doing:

Python code:
plotter.layers[RED_LAYER].add_line(0, 0, 50, -50)

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug

huhu posted:

I have two classes Plotter and Layer. A Plotter consists of an array of layers. Each layer is an array of instructions.

code:
class Plotter:
    def add_layer(self, name: str):
        self._layers[name] = Layer(self)

    def update_layer(self, name: str) -> Layer:
        return self._layers[name]
code:
class Layer:
    def add_line(self, x1, y1, x2, y2):
        self.add_pen_down_point(x1, y1)
        self.add_point(x2, y2)
I've removed a bunch of code for brevity.

If I want to update a layer, I currently do the following:

code:
plotter.update_layer(RED_LAYER).add_line(0, 0, 50, -50)
This feels bad, is there a more pythonic way?

You've already tightly coupled the two classes together pretty strongly, so Macichne Leainig's response is probably a good starting point.

Although, using __getitem__ to give you dictionary style access is better than update_layer (not simply because update_layer is misleadingly named; a function called update should...well update something, not return that something. At the very least you should rename it to get_layer). This would simplify it slightly further and looks a lot cleaner than the original example:

Python code:
plotter[RED_LAYER].add_line(0, 0, 50, -50)
Ex: https://geekpython.in/implement-getitem-setitem-and-delitem-in-python#:~:text=The%20__getitem__%20is,__getitem__(key)%20.

The downside is that this would be bad if you wanted to use that get_item for something else, or you had a bunch of various options like layers, in which case then yeah, just expose the layer.

FWIW: Remember that "private" doesn't mean anything real in Python, so I generally only use the _ prefix for stuff that wouldn't ever be useful to another piece of code, like decomposed methods or things like that, or client objects you want to at least ward people away from.


huhu posted:

And one more question. I've got a directory structure

code:
project
    main.py
    __init__.py
demos
    demo1.py
    __init__.py
I want to import project into demos. However, when I cd into the demos directory and try and run any variation of import statements, I get various errors. I've Googled a bunch and I cannot figure out what's wrong with my import. Or is there a better way to make a demos directory for a package I'm building? One of the stack overflow answers says
code:
If the directories aren't Python packages, you can do this by messing around with sys.path, but you shouldn't.

So I feel like I'm missing something here.

I'll be honest, Python imports are kind of a murky soup to me, despite working with it a bunch. I would also be interested if anyone had a video/blog post or something that explained it without getting into the deep minutiae of import logic, simply the 'if you run from X you pull in Y' level stuff.

huhu
Feb 24, 2006
I added this gross code:

code:
sys.path.append(os.path.abspath('../project'))
from Project import Project
Which now works except VSCode now says `Import "Project" could not be resolvedPylance` but the code still excutes.

I swear, between installing a virtual environment and getting imports, I end up just abandoning Python if my code grows out of a single file with basic imports.

Macichne Leainig
Jul 26, 2012

by VG
That I think is because Python doesn't like you importing from parent-level directories, which judging from your described file structure seems like it could be the case.

I think you want something more like:

code:
project
    main.py
    module_1
        stuff.py
        __init__.py
Then from the main.py in project you should be able to just do something like:

code:
from module_1 import stuff
Otherwise yeah you have to hack it by adding the module's parent directory to Python's environment variable which as you noted is not pretty. You can make it slightly less hardcoded by using builtins like __file__ but you're probably just better off reorganizing your folder structure.

Zoracle Zed
Jul 10, 2001

huhu posted:

I want to import project into demos. However, when I cd into the demos directory and try and run any variation of import statements, I get various errors. I've Googled a bunch and I cannot figure out what's wrong with my import. Or is there a better way to make a demos directory for a package I'm building? One of the stack overflow answers says
code:
If the directories aren't Python packages, you can do this by messing around with sys.path, but you shouldn't.

So I feel like I'm missing something here.

You'll probably want to follow a guide like this. This will allow you to install the project path permanently to the python path. In short:

1. Create a 'src' subfolder in the root folder and move the project folder there
2. Create a pyproject.toml file in the root folder and add the appropriate contents
3. In the root folder, run 'pip install --editable .'

Then 'python demos/demo1.py' should work as expected. (The --editable part means you don't have to reinstall everything everytime you modify the code)

huhu
Feb 24, 2006

Zoracle Zed posted:

You'll probably want to follow a guide like this. This will allow you to install the project path permanently to the python path. In short:

1. Create a 'src' subfolder in the root folder and move the project folder there
2. Create a pyproject.toml file in the root folder and add the appropriate contents
3. In the root folder, run 'pip install --editable .'

Then 'python demos/demo1.py' should work as expected. (The --editable part means you don't have to reinstall everything everytime you modify the code)

Ahhhhh, this was what I was looking for. Thank you!

StumblyWumbly
Sep 12, 2007

Batmanticore!

huhu posted:

I have two classes Plotter and Layer. A Plotter consists of an array of layers. Each layer is an array of instructions.

code:
class Plotter:
    def add_layer(self, name: str):
        self._layers[name] = Layer(self)

    def update_layer(self, name: str) -> Layer:
        return self._layers[name]
code:
class Layer:
    def add_line(self, x1, y1, x2, y2):
        self.add_pen_down_point(x1, y1)
        self.add_point(x2, y2)
I've removed a bunch of code for brevity.

If I want to update a layer, I currently do the following:

code:
plotter.update_layer(RED_LAYER).add_line(0, 0, 50, -50)
This feels bad, is there a more pythonic way?


A pattern I like (could be bad, actually, and I'm interested in other opinions on it) is to have add_line return itself, so if you want to add multiple lines you could do:
code:
plotter.update_layer(RED_LAYER).add_line(0, 0, 50, -50) \
                                                           .add_line(1, 2, 3, -50) \
                                                           .add_line(10, 20, 30, -50)
That can be combined with the other ideas folks have mentioned.
Depending on how the code grows, this can make life better or worse.

Macichne Leainig
Jul 26, 2012

by VG
I don't think it's a bad idea or anything. If anything it saves you from having to write the same boilerplate to access that class member over and over again.

Might be confusing initially if someone hasn't encountered the pattern, but I don't think it's like, an antipattern or anything like that.

Armitag3
Mar 15, 2020

Forget it Jake, it's cybertown.


It's called fluent interface design, and it's not a bad idea at all, especially applied to situations where the flow can read logically from one operation to the next.
Python code:
# 1 + 2 - 3 + 4
result = calculator.add(1).add(2).subtract(3).add(4).result()

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug

Armitag3 posted:

It's called fluent interface design, and it's not a bad idea at all, especially applied to situations where the flow can read logically from one operation to the next.
Python code:
# 1 + 2 - 3 + 4
result = calculator.add(1).add(2).subtract(3).add(4).result()

I quite liked this in LINQ in C#, although I do agree it's not used much in Python. I don't think that's an antipattern or anything though, just convention.

KICK BAMA KICK
Mar 2, 2009

Pretty common pattern for ORMs where query methods return objects that can themselves be further queried, Django's works that way.

Adbot
ADBOT LOVES YOU

Chin Strap
Nov 24, 2002

I failed my TFLC Toxx, but I no longer need a double chin strap :buddy:
Pillbug
Used all the time in Pandas too

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply