#python2 syntax, now throws an error

print "hello world"

#python3 syntax, this also works in python2 (2.5+) though in python3 this is the only option

print("hello world")

#documentation on the python3 print function
help(print)

"""
Explanation: Coding in Python3
So now that PmagPy has made the conversion to python3, for at least a short time the command line programs will be supported in both Python2 and Python3 using the library future which can be installed by in your favorite package manager (canopy, anaconda) or using this code in the command line pip install future. This is not true for the GUIs, however, which due to their dependency on the wxpython library must be in one language or the other. For the sake of future proofing the library all GUI related code needs to be in Python3 as soon many of the scientific libraries (ipython, matplotlib, pandas) required by PmagPy are dropping support for Python2. A full list of libraries dropping support for Python2 by 2020 can be found here.
Python3 vs. Python2
There are a number of differences between the two programming languages, which while not completely unrelated are disparate. This guide will go over those changes most relevant to PmagPy development as follows:

Print function vs print statement
Relative imports
Exception raising/catching
Strings
Generator vs. List vs. Iterable Objects(range, map, filter, {}.keys(), {}.values, {}.items())
input vs. raw_input
Division

This is by no means a full list of the differences between the languages and a more comprehensive list can be found here. The goal of this list is to be concise and simple for those developing PmagPy rather than thorough.
Note: if you already have Python2 code you would like to see incorporated into the GUI see Converting Python2 Code
Print Function vs. Print Statement
<a id='print_explanation'></a>
This is the most simple of the differences between Python2 and Python3 and the most commonly encountered. Simply the print statement in Python3 is only a function not a statement like in Python2. This means that there is no special syntax for print and it must be called as a function would. This also means that there are key word arguments for print to allow better manipulation of text.
End of explanation
"""


# relative import in Python 2 only:
import submodule2
# relative import in Python 2 and 3:
from . import submodule2

# However, absolute imports are (in my opinion) easier and simpler.
# Absolute imports mean you specify the entire package name, i.e.:

from my_package import submodule2
# or
import my_package.submodule2 as mod2

# so feel free to stick to using them, instead

"""
Explanation: Relative Imports
<a id='import_explanation'></a>
http://python-future.org/compatible_idioms.html#imports-relative-to-a-package
End of explanation
"""


#Python2 rasing an error

raise TypeError, "Expected a diblock was given type(%s)"%str(type("")) #gives SyntaxError no the TypeError we wanted

#Python3 raising an error

raise TypeError("Expected a diblock was given type(%s)"%str(type("")))#gives the appropriate TypeError

#Python2 catching an error

try:
    raise RuntimeError; print("obviously not caught")
except RuntimeError, err:
    print(err, "caught the error: note there's no message for the error")

#Python3 catching an error

try:
    raise RuntimeError; print("obviously not caught")
except RuntimeError as err:
    print(err, "caught the error: note there's no message for the error")

"""
Explanation: Exception Raising/Catching
<a id='exception_explanation'></a>
This is simply a syntax change in the raising and catching syntax.
End of explanation
"""


#Some Python3 examples of Unicode vs. Bytes

print('strings are now utf-8 \u03BCnico\u0394é!', type(''))

print(b'bytes are a thing now too and when turned into a string keep this b in front', type(b' bytes for storing data'))


"""
Explanation: Strings
<a id='string_explanation'></a>
One of the most unnoticed and important changes between Python2 and Python3 is the difference in Strings and how they are encoded. Def: encoding - the manner in which the bits are organized to represent a given piece of information to the computer, in this case string characters. Python2 used ASCII strings by default and had a class Unicode, where as Python3 uses Unicode as the main string class (str) and has two other string classes byte and bytearray which are used to represent binary data. This mostly causes a problem in PmagPy when reading binary data using open(file,'b') as it will now be read in as a byte object which cannot be manipulated like a string. This can be seen in the 2G binary conversion script in the programs directory. This can also be a problem when using libraries like json as the library may read in using a different encoding like ASCII and need to be decoded to turn into the correct string. An example of this can be seen in data_model3 in the pmagpy directory. This is rather case by case and something only occasionally run into, hopefully the work arounds in data_model3 and the 2G binary conversion script can help you overcome most of these issues.
End of explanation
"""


print("python3 range output")
p3r=range(4)
print(p3r,type(p3r),'\n')

print("casting range object to list to simulate python2 output")
p2r=list(range(4))
print(p2r,type(p2r))

#Other examples

print("python3 map output")
p3m = map(lambda x: x+1, range(4))
print(p3m, type(p3m))
print(list(p3m),type(list(p3m)),'\n')

print("python3 filter output")
p3f = filter(lambda x: (x%2)==0, range(4))
print(p3f, type(p3f))
print(list(p3f),type(list(p3f)),'\n')

print("python3 dictionary methods with new classes for output")
p3d = {"thing1": 1, "thing2": "hello world", "thing3": 3.75, 5.47: "another value"}
print(p3d, type(p3d),'\n')

p3dk = p3d.keys()
p3dv = p3d.values()
p3di = p3d.items()

print(p3dk, type(p3dk))
print(list(p3dk),type(list(p3dk)),'\n')

print(p3dv, type(p3dv))
print(list(p3dv),type(list(p3dv)),'\n')

print(p3di, type(p3di))
print(list(p3di),type(list(p3di)),'\n')

"""
Explanation: Generators vs. Lists vs. Tuples
<a id='generator_list_explanation'></a>
This is a pain in the rear of a change. As the python vision is to be as explicit as possible and make objects for everything, Python3 is extremely explicit on what things are what objects and there are a lot more objects. For instance in Python2 range(4) returns a list [0,1,2,3] where as in Python3 range(4) returns a range object which is iterable and decended from the generator class, but does not have the same methods as a list so you cannot try append to it, and has more methods than the generator class.
End of explanation
"""


def make_gen():
    x=0
    while True:
        yield x
        x+=1
gen=make_gen() #makes a generator that returns all non-negative integers
print(gen, type(gen))
print(next(gen)) #note in Python2 this was done gen.next() though in Python3 next is an external function not a method
print([next(gen) for _ in range(20)])
print(next(gen))
print(hasattr(gen,'__next__'),'\n')

ran20 = range(20) #returns a range object which is related to a generator, but different as it has no next, remembers data, and can be indexed
print(ran20, type(ran20))
print(ran20[0])
print(list(ran20))
print(ran20[0])
print(hasattr(ran20,'__next__'),'\n') #this means you can't call next(ran20)
#manipulating this object and remembering it's difference is often quite a pain so it is sometimes better to just turn it into a list


#This is meant to demonstrate how lists mutate and show the difference betweeen tuples and lists
print("A = list(range(5)) : creates a list of the first 5 non-negative integers")
A = list(range(5))
print("B = A : makes B point to A")
B = A
print("C = list(A) : makes a copy of A in C")
C = list(A)
print("A = ", A)
print("B = ", B)
print("C = ", C, '\n')

print('B[0] = "haha" : Notice how a change to B also changes A')
B[0] = "haha"
print("A = ", A)
print("B = ", B)
print("C = ", C, '\n')

print("A[2] = 5.938 : and vice versa, this is one aspect of mutation")
A[2] = 5.938
print("A = ", A)
print("B = ", B)
print("C = ", C, '\n')

print("C[4] = True : Though C which is a copy not a pointer is not changed")
C[4] = True
print("A = ", A)
print("B = ", B)
print("C = ", C, '\n')

#you can check these kind of things using the "is" statement without needing to go through all the above changes

print("reset A, B, C as in first step")
A = list(range(5)) #creates a list of the first 5 non-negative integers
B = A #makes B point to A
C = list(A) #makes a copy of A in C

print("A is B : ", A is B)
print("A is C : ", A is C)
print("A==B==C : ", A==B==C)

#Trying the above exercise again with Tuples to demonstrate immutability
print("A = tuple(range(5)) : creates a list of the first 5 non-negative integers")
A = tuple(range(5))
print("B = A : makes B a copy of A")
B = A
print("C = tuple(A) : makes C a copy of A")
C = tuple(A)
print("A = ", A)
print("B = ", B)
print("C = ", C, '\n')

#again we can use is to determine what is a copy and what is identical
print("A is B : ", A is B)
print("A is C : ", A is C)
print("A==B==C : ", A==B==C)
print("in this case all are identical as there is no need to make a copy of a tuple as it can't change\n")

print('B[0] = "haha"')
print("And ERROR, because you can't do this to a tuple which prevents the headache above from developing")
B[0] = "haha"

"""
Explanation: Explination of the difference between Generators, Lists, and Tuples
Note: If you plan to just turn all of the new above mentioned data types into lists then you can probably safely skip this, but the bellow ilistrates important distinctions between the different iterable types in both Python2 and Python3 and should help you write clearer code with the right objects used for all cases.
This difference brings up a conversation on the 3 different types of objects which contains sets of data as they can be found in both Python2 and Python3: generators, lists, and tuples. Generators are objects which have a current state and a defined next operation (i.e. x=0, x+1) this allows you to define infinite sets or large sets without storing all the data in memory. Lists are built-in arrays which contain each piece of data in RAM and can access them as requested by the user (i.e. [0,2,3,4,5]), most importantly lists are mutable so their values can be changed even in different namespaces than they were created. Tuples are nearly identical to lists, however, they are imutable and they must be recreated to change even a single value. This distinction is more important in Python3 than Python2 as many of the data types returned from the built-in functions above are decended from the generator class or the tuple class instead of just returning a list as Python2 does. Here are some examples of a basic generator, list, and tuple.
End of explanation
"""


# Python 2
# does not exist in Python 3
raw_input('give me a variable')

# Python 3
# this is equivalent to Python 2's raw_input and is safe (i.e., will not be evaluated by Python)
input('give me a number')

"""
Explanation: input vs. raw_input
<a id='input_explanation'></a>
End of explanation
"""


#Python2 example output

example=\
"""
3 / 2 = 1
3 // 2 = 1
3 / 2.0 = 1.5
3 // 2.0 = 1.0
"""

print(example)

#Python3 test of Python2 example code

print("3 / 2 =", 3 / 2)
print("3 // 2 =", 3 // 2)
print("3 / 2.0 =", 3 / 2.0)
print("3 // 2.0 =", 3 // 2.0)

"""
Explanation: Division
<a id='division_explanation'></a>
This is a rather subtle change to Python and can often go unnoticed as it doesn't raise an error. The main change is that in Python2 int/int = int every time, however in python3 int/int = int_or_float. Here are some various examples of both division (/) and whole number division (//).
End of explanation
"""


