ccubed / PyMoe

Welcome to Pymoe, the only python lib you'll ever need if you need anime/manga on the python platform.

Home Page:https://ccubed.github.io/PyMoe/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Consider NT_STATUS Constants?

lethargilistic opened this issue · comments

As it stands now, a user of PyMoe has to look up the potential values of NT_STATUS, the NT that shows the show's airing status and the user's watching status on MAL.

A PyMoe user would then use these magic numbers and strings, or they'd make constants to represent them. I think PyMoe should just provide these constants. I've thought of some options, though I don't like any of them 100%. There's some I like 0%, but they're still possibilities. Feel free to offer alternatives.

My Format Ideas

For user (Anime and manga constants are the same):

  • NT_STATUS.USER_COMPLETED
  • User.COMPLETED
  • UserStatus.COMPLETED (New class)
  • Status.User.COMPLETED (New class)
  • Status.USER_COMPLETED (New class)
  • Status.COMPLETED (New class)

For series (Anime and manga constants are different):

  • NT_STATUS.ANIME_FINISHED and NT_STATUS.MANGA_FINISHED
  • Anime.FINISHED and Manga.FINISHED
  • AnimeStatus.FINISHED and MangaStatus.FINISHED (New class)
  • Status.Anime.FINISHED and Status.Manga.FINISHED (New class)
  • Status.ANIME_FINISHED and Status.MANGA_FINISHED (New class)

Consistency for Series

For series, I think it would be better if we settled on a consistent API that works for both. Like MAL should have if they were really sure about using strings for this. Here's my suggestions:

  • Finished Airing / Finished -> FINISHED
  • Currently Airing / Publishing -> STARTED
  • Not Yet Airing / Not Yet Published -> PLANNED

Doc update?

I've also made a commit adding the status code meanings to the docs. I have no idea if it would look good on the actual readthedocs page, so I'm not planning to submit a PR for that. If this issue is implemented, then it would be out of date immediately, anyway.

I don't really see this as an issue. Using an API brings with it some technicalities you have to deal with. The status values are one of them. I could make my own values for them, but that's non standard and now I've made a second standard in addition to the API that people need to adjust to. Additionally, some Apps may want to do specific actions on the statuses you're suggesting we remove. So no on this.

Additionally, some Apps may want to do specific actions on the statuses you're suggesting we remove.

That wasn't what I was suggesting. I was saying that, if we were to have constants, we should make the ones for Anime and Manga more consistent. That doesn't remove any of the underlying functionality, but makes the functionality easier to use. Which is something you're already doing with all of the abstractions classes in MAL, by the way.

But if that's your final decision, fine.

These magic numbers and strings are still necessary for getting part of PyMoe's MAL wrapper to work, so they should at least be documented within PyMoe. Not doing so is asking users to reference both your wrapper and the original API, without even giving them a hint for where to look in the original API. Just providing the values straight up is even better than a hint.

To be honest, I'm not sure what magic numbers or strings you're even talking about. I return the same status strings as MAL. The only helper I have for those are two structures which contain a list of MAL status strings mapped to the IDs that The MAL api uses for the statuses.

See:
https://github.com/ccubed/PyMoe/blob/master/Pymoe/Mal/Abstractions.py#L118

So PyMoe was converting the status numbers returned by MAL to those strings. That was part of my confusion.

I'll try to explain my specific issue. Hopefully that will better show why I think this could be improved.

Like the docs say: buckle up.

Anime.status.series

I'm trying to filter a user's list of Anime by the show's status:

airing_anime = [x for x in user.anime.list if x.status.series == 'Currently Airing']

for show in airing_anime:
    print(show.status, show.title)

Why do I have to know that 'Currently Airing' is the key to filter by? Why is that a string that I could easily mistype? 'Currently Airng' runs just as well, but will make this return nothing. If this were a constant, misspelling it would stop the code and point me to the error.

By pointing me to STATUS_INTS_ANIME leads me to think you're suggesting this code:

from Pymoe.Mal.Abstractions import STATUS_INTS_ANIME
airing_anime = [x for x in user.anime.list if x.status.series == STATUS_INTS_ANIME[0]]

for show in airing_anime:
    print(show.status, show.title)

This code is not clear. It relies on me knowing three things:

  1. That this exists, which I didn't. There's nothing pointing from NT_STATUS to this in the docs, as it stands. In fact, it's not in the docs at all as far as I can tell. And the import is cryptic, too.
  2. The MAL status codes. What do I do if I'm mistaken about a number. I'd have to look up outside of PyMoe and its documentation. A wrapper aiming for comprehensiveness and ease of use should hide details like this (with constants) or at least make them easy to discover (linking out to the documentation you expect a user to know).
  3. That 0 is NOT the MAL status code for Currently Airing. It's that status code MINUS ONE so it fits into a zero-indexed array. That's an implementation detail I have to know to work around, not a mirror of the API.

And that's just the Anime side. Manga uses a different array of strings despite the fact that MAL returns integer codes 1 through 5 meaning the same things for both.

Actually, 'Dropped' (4, but STATUS_INTS_ANIME[3]) and 'Plan to Watch' (5, but STATUS_INTS_ANIME[4]) are in there, and those aren't values for a series at all. They're values that a user could have. Also, the MAL API returns 6 for 'Plan to Watch'. Or, at least, that's what I'm getting from Anime.status.user.

Anime.status.user

airing_anime = [x for x in user.anime.list if x.status.user == 1]

for show in airing_anime:
    print(show.status, show.title)

My criticism of the Anime.status.user begins with Point 2 of what I said for Anime.status.series, regarding the requirement of knowing MAL status codes. If I'm wrong about what a status code mean, I have to look beneath PyMoe's level of abstraction.

But there's another thing.

There is no STATUS_INTS_USER array (nor should there be), so not only do I not address user status codes with an array, but these status codes are NOT off by one. The API for NT_STATUS is fundamentally inconsistent.

Debugging

Moreover, take a look at the output of this code for my MAL:

NT_STATUS(series='Currently Airing', user=4) One Piece
NT_STATUS(series='Currently Airing', user=1) Detective Conan
NT_STATUS(series='Currently Airing', user=4) Naruto: Shippuuden
NT_STATUS(series='Currently Airing', user=6) Long Riders!
NT_STATUS(series='Currently Airing', user=6) Dragon Ball Super
NT_STATUS(series='Currently Airing', user=1) ClassicaLoid
NT_STATUS(series='Currently Airing', user=3) Drifters
NT_STATUS(series='Currently Airing', user=3) Udon no Kuni no Kiniro Kemari
NT_STATUS(series='Currently Airing', user=3) Shakunetsu no Takkyuu Musume

This is not clear. The status codes of one have been converted to string literals, and the other are integers. Why? The string 'Currently Airing' doesn't help me at all if I'm trying to use the suggested STATUS_INTS_ANIME. The integer shown by user doesn't help me if I haven't memorized the status codes from MAL, which are weird. Why do they start from 1? Why do they go 1,2,3,4,6? We can wrap around and hide that strangeness without diverging from what the API is saying.

Alternate Solutions

A quick solution

Get rid of STATUS_INTS_ANIME, because it obfuscates the MAL API.

Just let Anime.status.series be the MAL status code.

A better solution

Step 1: Easy

See first sentence of 'A quick solution'.

Step 2: Constant

I wouldn't force you to have constants, even if I could. I just want to show you what the code I'm envisioning looks like.

A Status module contains constants for Series and User. To get these constants, you can use one import:

from Pymoe.Mal.Status import Series, User

With this, you get access to

  • Series
    • Series.STARTED (replaces 'Currently Airing' and 'Publishing') (incidentally, 1)
    • Series.FINISHED (replaces 'Finished Airing' and 'Finished') (incidentally, 2)
    • Series.PLANNED (replaces 'Not Yet Aired' and 'Not Yet Published') (incidentally, 3)
  • User
    • User.WATCHING (incidentally, 1)
    • User.COMPLETED (incidentally, 2)
    • User.ONHOLD (incidentally, 3)
    • User.DROPPED (incidentally, 4)
    • User.PLANNED (replaces 'Plan To Watch', 'Plan to Read') (incidentally, 6)

Step 3: Profit

For a series status filter:

from Pymoe.Mal.Status import *
airing_anime = [x for x in user.anime.list if x.status.series == Series.STARTED]

for show in airing_anime:
    print(show.status, show.title)

For a user status filter:

from Pymoe.Mal.Status import *
airing_anime = [x for x in user.anime.list if x.status.user == User.WATCHING]

for show in airing_anime:
    print(show.status, show.title)

There is no ambiguity in this code. It says what it does, so we understand what it does. If we need to look up a constant, we don't have to go below PyMoe. Look at the docs for PyMoe.Mal.Status.

Step 4: __repr__ Profit

But what could it print out, if we take advantage of the fact that the string 'Series.STARTED' is a valid __repr__ for Series.STARTED?

That's when things get awesome.

NT_STATUS(series=Series.STARTED, user=User.DROPPED) One Piece
NT_STATUS(series=Series.STARTED, user=User.WATCHING) Detective Conan
NT_STATUS(series=Series.STARTED, user=User.DROPPED) Naruto: Shippuuden
NT_STATUS(series=Series.STARTED, user=User.PLANNED) Long Riders!
NT_STATUS(series=Series.STARTED, user=User.PLANNED) Dragon Ball Super
NT_STATUS(series=Series.STARTED, user=User.WATCHING) ClassicaLoid
NT_STATUS(series=Series.STARTED, user=User.ONHOLD) Drifters
NT_STATUS(series=Series.STARTED, user=User.ONHOLD) Udon no Kuni no Kiniro Kemari
NT_STATUS(series=Series.STARTED, user=User.ONHOLD) Shakunetsu no Takkyuu Musume

Look at that. I don't need to know any implementation details. It's all right there.

I want to debug that right now.

Step 4a: An edit and self-criticism

After thinking about this for a while, I don't think the __repr__ plan would work, because the constants I was thinking of up to this point would be integers. They don't have attributes like that.

Instead, embrace Python 3.4+, because it's time, and use enum.

To begin with, and this is blunt, but before you criticize the writing of the code, go read the API documentation: https://myanimelist.net/modules.php?go=api

The status codes go 1,2,3,4,6 because the status codes go 1,2,3,4,6. That's the MAL API. I did not create that, MAL created that. There is no 5. Why is there no 5? I don't know.

To address the concerns however. There are two lists because the codes are different. Manga has Reading and PlanToRead. Anime has Watching and PlanToWatch. You can say these are the same and maybe they are, but the options aren't as simple as saying these are equal.

So, on to the solutions.

  1. Remove the String representations.
    I don't want to do this because I don't want to force the programmer to need to know about the MAL API if they don't want to. If anything, I'm more inclined to just remove the integer.

  2. Constants
    Here's my problem with this. Say we do this. I can make constants easily enough. I don't have a problem with this. However, here's what happens with this.

Okay, I've made these constants. Now, how do we represent them as strings? We have to now account for the issue of read and watch in terms of anime versus manga. Oh? We don't want to do that? Okay, then i'll just return the ints. But wait, now i'm forcing the programmer to understand the MAL API (which is terrible) which I don't want to do. So constants go down a path I don't want to, which is a path of me constantly making new constants to support many different situations.

  1. List Comprehensions
    You can do this now. I'm not sure what's ambiguous about the following code.
[x for x in anime_list if x.status.series == "Currently Airing"]

Now I still don't understand your real problem. Your problem seems to be that you don't like using the strings and don't want to use the integer codes. Which is fine. However, You have to use one or the other. If anything, I'm going to remove the integers since I don't want to make the programmer learn the MAL API. It seems like this is searching for an answer to a problem that does not exist. What exactly is your issue here?

My problem

The status codes go 1,2,3,4,6 because the status codes go 1,2,3,4,6. That's the MAL API. I did not create that, MAL created that. There is no 5. Why is there no 5? I don't know.

I appreciate the bluntness, so let me return the favor: I did look the MAL API up before raising this issue because I couldn't believe it, and you saying "I don't know" here is part of the problem. Me (a user) or you (the owner) having to ask what 1,2,3,4,6 means is a valid criticism of PyMoe because PyMoe currently uses it. The MAL API designers made a stupid decision here.

My problem is that the wrapper following this API convention when it doesn't have to has already lead me to unnecessary overhead and confusion in my project. It brought me here, where I had to write an entire article for a developer.

This is a place where:

  • the MAL API is crap,
  • PyMoe is currently crap because it mirrors MAL too closely,
  • and it's one of those places where a small change could put PyMoe above other MAL wrappers that mirror crap from the original API.

Do you think requests got where it is today by mirroring urllib2 exactly then defending poor implementations because urllib2 was like that?

Code

In both of our hypothetical versions of PyMoe, this part of the API does not use integers for this purpose. We want to hide the MAL API implementation detail to keep a user productive and protect them from mistakes. Using strings for this is more productive than integers, but it does not protect them.

[x for x in anime_list if x.status.series == "Currently Airng"]

This is the example I used before, of a small misspelling in the filter string. This does not break the code. We're both programmers. I don't know about you, but I know I've spent huge amounts of time trying to figure out bugs caused by mistyping one character. Why leave a user of PyMoe open to this when you don't have to?

[x for x in anime_list if x.status.series == Series.STARTD]

This breaks quickly and you get a message telling you that it broke.

Now, your concern about how to display these to a user is a valid one. It's also simple to fix.

One way is that you could override __str__ in each enum, as shown in the docs. In anything that's user-facing, you could return the string representation of the Status.

This would be trivial for the strings that don't differ between Anime and Manga.

>>>str(User.WATCHING)
Watching

But your concern is for the cases where Anime and Manga do differ, as with Currently Watching/Airing. That would get more complicated, and what benefit does this developer headache bring to a user? Some sort of complex compromise solution that has to be explained to them and apologized for when it leads a user to make a mistake?

Here's the real fix: I'm not convinced returning any string for these values is a job for PyMoe.

The user knows whether or not this value refers to an Anime or Manga, based on the code they wrote.

Let's take another look at that list comprehension:

[x for x in anime_list if x.status.series == Series.STARTED]

What is this for? An anime_list? Seems like an anime. If you looked higher in the code, you'd see something that amounts to one of these lines:

  • instance.user('lethargilistic').anime.list
  • instance.user('lethargilistic').manga.list.

You already know which it was.

currently_airing = [x for x in anime_list if x.status.series == Series.STARTED]
for show in currently_airing:
    print(show.name, 'is Currently Airing')

This clarity is a great thing about PyMoe's MAL wrapper. We should trust in that. It's better to just return the enum and allow a user to convert it to a str if that's what they want.

Also, consider this: the MAL API does not return strings. It returns ints. The MAL API already expects users to determine whether or not they're dealing with an Anime or Manga themselves. PyMoe returning a string is an extra step on top of that, and it leads to the current expectation of code that's easily breakable.

I'll spare you repeating what I said about constants and simply go straight to the problem with enums.

  1. If I was going to remove the strings entirely, I'd rather leave just the integers in. This is an issue with consistency. The integers are at least more consistent with the API than what I would have to do with Enums.

  2. Assuming I did create Enums here, which would be a 0 effort thing to do given how the code works, my biggest issue with this approach is the same issue I had with constants. It's more than just the difference between Reading and Watching. I now need to convert "Currently Airing" or "Not Yet Aired" or "Finished Airing" into new values. Values I've already thought of, to be clear: Airing, Finished, Unaired, Dropped and Planned. However, that's now a third set of values separate from the MAL API that someone needs to know. Thankfully with Enums you can simply ask for the value and get the integer back, but that falls back to needing to know the MAL API values. At the point though that I'm expecting people to fall back to the integer values, I'd rather just leave the integer values in and remove the strings, as stated.

  3. You keep coming back to this error of a misspelling in a filter string, but I'm not convinced this is my problem. I can't account for every programmer's mistake. I just can't. I would be creating checks on checks. There is a list of filters available if someone wants to use them or double check their strings.

  4. I recognize, however, that User should either be a string or not be present. I don't remember entirely why user status is an int. I probably meant to find out exactly which numbers correspond to which statuses and never did it, so that's on me.

So, to recap:

  1. I don't want to create a third set of values that someone needs to know.
  2. Enums have the issue of creating that third set of values (Unless I just have stupidly long names like Currently_Airing I guess)
  3. Misspellings in list comprehensions really aren't my problem
  4. I will make the user status a string barring another solution being discovered in this thread

Because I had an idea, it was interesting, and also I'm a masochist, I implemented some SeriesStatus classes and a method to generate the correct one that also handles whether it's an Anime or Manga.

I didn't end up using enum, since subclasses worked.

I do not think this is the right direction. It's convoluted.

Edit: I never got a notification regarding your last reply.

It's not a "third" set of values. It's a replacement for the second.

The strings like 'Currently Airing' are a second set of values that you've added on top of MAL's API. That they correspond to the literal names of the categories on MAL is incidental at best. My idea is to replace that second set entirely with a different, still second set that prevents errors.

  • A user with your version would need to know the string 'Currently Airing' for Anime and 'Publishing' for Manga.
  • A user with my version would need to know the constant 'Series.STARTED' for either.

If you don't consider a change that prevents user error and requires remembering fewer keywords as better, then I don't know what to tell you.

If you don't think a user running into an issue because of your design decision is your problem, then I guess that's just a fundamental difference in our development philosophies.

At the end of the day, at least make it consistent.

The strings like 'Currently Airing' are a second set of values that you've added on top of MAL's API. That they correspond to the literal names of the categories on MAL is incidental at best.

These strings are literally returned from the API. They are not a second set of values I added on top of the API, they are a set of values from the API.

This issue seems to boil down to wanting me to make the API go away entirely, which I cannot do. If you want to use the MAL API, as terrible as it is, you have to at least put some effort into understanding it. This is true of any API. I will make code changes to make user return a string instead of the status int. However, I'm not going to make another class or a third set of values when I'm using the values from the MAL API and they work fine.

commented

These strings are literally returned from the API. They are not a second set of values I added on top of the API, they are a set of values from the API.

That's quite not true. As we can see, you clearly define them yourself in Pymoe/Mal/Abstractions.py on line 118, and you then proceed to use them in Pymoe/Mal/__init__.py with status_anime=STATUS_INTS_ANIME[int(item.find('series_status').text)-1] if item.find('series_status').text != '6' else STATUS_INTS_ANIME[4],. We can even view the git blame of the Abstractions.py file to see that you actually put these strings in there yourself.

What I believe @lethargilistic is suggesting, which you should reconsider, is to replace these string variables in your Abstractions.py with an enum or enum-like constants, which would then be used everywhere instead of the currently used strings.

However, I'm not going to make another class or a third set of values when I'm using the values from the MAL API and they work fine.

You wouldn't need to make a third set of values though! :) As mentioned, you should get rid of the current "second set" completely and replace it with a better second set of enums and constants.

That's quite not true. As we can see, you clearly define them yourself in Pymoe/Mal/Abstractions.py on line 118 and you then proceed to use them

Example Response:

<?xml version="1.0" encoding="utf-8"?>
<anime>
  <entry>
    <id>2889</id>
    <title>Bleach - The DiamondDust Rebellion</title>
    <english>Bleach: Diamond Dust Rebellion</english>
    <synonyms>Bleach: The Diamond Dust Rebellion - M&Aring; 
    Bleach - The DiamondDust Rebellion - Mou Hitotsu no Hyourinmaru</synonyms>
    <episodes>1</episodes>
    <type>Movie</type>
    <status>Finished Airing</status>
    <start_date>2007-12-22</start_date>
    <end_date>2007-12-22</end_date>
    <synopsis>A valuable artifact known as &amp;quot;King's Seal&amp;quot; is stolen 
    by a mysterious group of people during transport in Soul Society. Hitsugaya Toushiro, 
    the 10th division captain of Gotei 13, who is assigned to transport the seal fights the 
    leader of the group and shortly after goes missing. After the incident, Seireitei declares 
    Hitsugaya a traitor and orders the capture and execution of Hitsugaya. Kurosaki Ichigo 
    refuses to believe this, and along with Matsumoto Rangiku, Kuchiki Rukia and Abarai Renji 
    swear to uncover the real mastermind of the stolen seal, find Hitsugaya and clear his name. 
    Meanwhile, a rogue Hitsugaya searches for the perpetrators and uncovers a 
    dark secret regarding a long dead shinigami. (from ANN)</synopsis>
    <image>https://myanimelist.cdn-dena.com/images/anime/6/4052.jpg</image>
  </entry>
</anime>            

Directly from MAL API docs, please read before you comment @Mahi @ccubed may define them, but they're 100% from the API already anyways.

commented

@getrektbyme It might be that the MAL API provides such response, I don't argue about that, but it's completely irrelevant to the Python code used in PyMoe. The Python side defines the strings as a layer on top of the integers, when it should use constants/enum instead.

As we can see on Mal/__init__.py line 333, the data is received as integer status code. Copying the status strings from Mal is nothing but bad design which should be avoided.

The only place integers are received is the user endpoint. In any case, why are we still discussing a closed issue. I have enforced consistency by making user status a string.